@deneb-ui/cli 2.0.85 → 2.0.86
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 +39 -2
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +1145 -0
- package/src/arc/adapters/accordion.cjs +32 -0
- package/src/arc/adapters/dialog.cjs +26 -0
- package/src/arc/adapters/embla.cjs +29 -0
- package/src/arc/adapters/gallery.cjs +23 -0
- package/src/arc/adapters/picture-source.cjs +71 -0
- package/src/arc/adapters/registry.cjs +156 -0
- package/src/arc/adapters/slick.cjs +26 -0
- package/src/arc/adapters/swiper.cjs +29 -0
- package/src/arc/adapters/tabs.cjs +29 -0
- package/src/arc/adapters/ui-frameworks.cjs +140 -0
- package/src/arc/adapters.cjs +20 -151
- package/src/arc/canonical-paths.cjs +95 -0
- package/src/arc/data-flow.cjs +158 -0
- package/src/arc/diff.cjs +136 -0
- package/src/arc/explain.cjs +113 -0
- package/src/arc/field-paths.cjs +25 -13
- package/src/arc/index.cjs +118 -6
- package/src/arc/ir-builder.cjs +581 -0
- package/src/arc/manifest.cjs +18 -0
- package/src/arc/planner.cjs +106 -2
- package/src/arc/printer.cjs +18 -5
- package/src/arc/residual.cjs +9 -3
- package/src/arc/rsc-boundary.cjs +154 -0
- package/src/arc/runtime-validator.cjs +286 -0
- package/src/arc/semantic.cjs +217 -16
- package/src/arc/text-fragment-analyzer.cjs +173 -0
- package/src/arc/transformer.cjs +627 -9
package/src/arc/adapters.cjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
const { collectJsxText, getJsxAttributeLiteral } = require('./ast.cjs');
|
|
4
|
+
const {
|
|
5
|
+
AdapterRegistry,
|
|
6
|
+
defaultRegistry,
|
|
7
|
+
BUILTIN_ADAPTERS,
|
|
8
|
+
registerAdapter,
|
|
9
|
+
getAdapter,
|
|
10
|
+
getAllAdapters,
|
|
11
|
+
activeAdapters,
|
|
12
|
+
recognizeWithAdapters,
|
|
13
|
+
resolveActionWithAdapters,
|
|
14
|
+
} = require('./adapters/registry.cjs');
|
|
12
15
|
|
|
13
16
|
const ICON_NAME_RE = /^(Icon|[A-Z][A-Za-z0-9]*(Icon|Logo)|Lucide[A-Z]|HiOutline|HiSolid|Fa[A-Z]|Md[A-Z]|Io[A-Z])/;
|
|
14
17
|
const DECORATIVE_TAGS = new Set([
|
|
@@ -22,151 +25,11 @@ const SKIP_TAGS = new Set([
|
|
|
22
25
|
const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'Heading', 'Title', 'CardTitle', 'ModalHeader', 'DialogTitle']);
|
|
23
26
|
const TEXT_TAGS = new Set(['p', 'span', 'li', 'blockquote', 'figcaption', 'label', 'CardDescription', 'Description', 'Subtitle', 'Typography', 'Text', 'Badge', 'badge']);
|
|
24
27
|
const ACTION_TAGS = new Set(['a', 'Link', 'NavLink', 'Button', 'button', 'IconButton', 'NavbarBrand']);
|
|
25
|
-
const IMAGE_TAGS = new Set(['img', 'Image', 'Img', 'BackgroundImage']);
|
|
28
|
+
const IMAGE_TAGS = new Set(['img', 'Image', 'Img', 'BackgroundImage', 'picture']);
|
|
26
29
|
const FORM_INPUT_TAGS = new Set(['input', 'Input', 'textarea', 'Textarea', 'select', 'Select']);
|
|
27
30
|
const FORM_CONTAINER_TAGS = new Set(['form', 'Form']);
|
|
28
31
|
|
|
29
|
-
const
|
|
30
|
-
detect: (project) => detectFromProfile(project, 'shadcn') || project.shadcn,
|
|
31
|
-
recognizeNode(node, ctx) {
|
|
32
|
-
const name = getJsxName(node);
|
|
33
|
-
if (name === 'Button') {
|
|
34
|
-
return { library: 'shadcn', kind: 'button', asChild: hasJsxAttribute(node, 'asChild') };
|
|
35
|
-
}
|
|
36
|
-
if (name === 'CardTitle' || name === 'CardDescription' || name === 'Badge') {
|
|
37
|
-
return { library: 'shadcn', kind: 'text', tag: name };
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
},
|
|
41
|
-
resolveAction(node) {
|
|
42
|
-
const name = getJsxName(node);
|
|
43
|
-
if (name === 'Button' && hasJsxAttribute(node, 'asChild')) {
|
|
44
|
-
const child = (node.children || []).find((c) => c && c.type === 'JSXElement');
|
|
45
|
-
if (child && ACTION_TAGS.has(getJsxName(child))) return child;
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const heroui = createAdapter('heroui', {
|
|
52
|
-
detect: (project) => detectFromProfile(project, 'heroui') || project.heroui,
|
|
53
|
-
recognizeNode(node) {
|
|
54
|
-
const name = getJsxName(node);
|
|
55
|
-
if (['Button', 'Link', 'Navbar', 'NavbarBrand', 'NavbarItem', 'NavbarContent'].includes(name)) {
|
|
56
|
-
return { library: 'heroui', kind: name === 'Button' || name === 'Link' ? 'action' : 'structure', tag: name };
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
},
|
|
60
|
-
resolveAction(node) {
|
|
61
|
-
const name = getJsxName(node);
|
|
62
|
-
if (name === 'Button' && (hasJsxAttribute(node, 'href') || hasJsxAttribute(node, 'as'))) return node;
|
|
63
|
-
if (name === 'Link') return node;
|
|
64
|
-
return null;
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const nextjs = createAdapter('nextjs', {
|
|
69
|
-
detect: (project) => project.framework === 'nextjs',
|
|
70
|
-
recognizeNode(node) {
|
|
71
|
-
const name = getJsxName(node);
|
|
72
|
-
if (name === 'Link') return { library: 'nextjs', kind: 'action', tag: 'Link' };
|
|
73
|
-
if (name === 'Image') return { library: 'nextjs', kind: 'image', tag: 'Image' };
|
|
74
|
-
return null;
|
|
75
|
-
},
|
|
76
|
-
resolveAction(node) {
|
|
77
|
-
return getJsxName(node) === 'Link' ? node : null;
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const framer = createAdapter('framer-motion', {
|
|
82
|
-
detect: (project) => (project.animationLibraries || []).includes('framer-motion'),
|
|
83
|
-
recognizeNode(node) {
|
|
84
|
-
const name = getJsxName(node);
|
|
85
|
-
if (name.startsWith('motion.')) {
|
|
86
|
-
const tag = name.slice('motion.'.length);
|
|
87
|
-
return { library: 'framer-motion', kind: HEADING_TAGS.has(tag) || TEXT_TAGS.has(tag) ? 'text' : 'structure', tag };
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const reactBits = createAdapter('react-bits', {
|
|
94
|
-
detect: (project) => project.reactBits,
|
|
95
|
-
recognizeNode(node) {
|
|
96
|
-
const name = getJsxName(node);
|
|
97
|
-
if (/SplitText|BlurText|GradientText|ShinyText|CountUp|RotatingText/.test(name)) {
|
|
98
|
-
return { library: 'react-bits', kind: 'text', tag: name };
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const deneb = createAdapter('deneb', {
|
|
105
|
-
detect: (project) =>
|
|
106
|
-
(project.componentLibraries || []).includes('@deneb-ui/ui') ||
|
|
107
|
-
project['@deneb-ui/ui'] === true ||
|
|
108
|
-
Boolean(project.dependencies && project.dependencies['@deneb-ui/ui']),
|
|
109
|
-
recognizeNode(node) {
|
|
110
|
-
const name = getJsxName(node);
|
|
111
|
-
if (['EditableContactForm', 'ContactForm'].includes(name)) {
|
|
112
|
-
return { library: 'deneb', kind: 'form', tag: name };
|
|
113
|
-
}
|
|
114
|
-
if (['EditableGoogleFeedback', 'EditableCustomerReviews', 'CustomerReviews', 'GoogleFeedback'].includes(name)) {
|
|
115
|
-
return { library: 'deneb', kind: 'feedback', tag: name };
|
|
116
|
-
}
|
|
117
|
-
if (['EditableTestimonialSection', 'EditableTestimonialCard', 'TestimonialSection', 'Testimonials'].includes(name)) {
|
|
118
|
-
return { library: 'deneb', kind: 'testimonial', tag: name };
|
|
119
|
-
}
|
|
120
|
-
if (name === 'EditableMap' || name === 'Map') {
|
|
121
|
-
return { library: 'deneb', kind: 'map', tag: name };
|
|
122
|
-
}
|
|
123
|
-
if (['EditableImage', 'EditableText', 'EditableCard', 'EditableProductCard', 'Image', 'Text', 'Card'].includes(name)) {
|
|
124
|
-
return { library: 'deneb', kind: 'editable-primitive', tag: name };
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const radix = createAdapter('radix', {
|
|
131
|
-
detect: (project) => (project.componentLibraries || []).includes('radix'),
|
|
132
|
-
recognizeNode(node) {
|
|
133
|
-
const name = getJsxName(node);
|
|
134
|
-
if (name === 'Slot' || name.endsWith('.Root') || name.endsWith('.Trigger')) {
|
|
135
|
-
return { library: 'radix', kind: 'structure', tag: name };
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const ADAPTERS = [nextjs, deneb, shadcn, heroui, framer, reactBits, radix];
|
|
142
|
-
|
|
143
|
-
function activeAdapters(profile) {
|
|
144
|
-
return ADAPTERS.filter((adapter) => {
|
|
145
|
-
try {
|
|
146
|
-
return adapter.detect(profile);
|
|
147
|
-
} catch {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function recognizeWithAdapters(node, ctx, adapters) {
|
|
154
|
-
for (const adapter of adapters) {
|
|
155
|
-
if (!adapter.recognizeNode) continue;
|
|
156
|
-
const result = adapter.recognizeNode(node, ctx);
|
|
157
|
-
if (result) return { ...result, adapterId: adapter.id };
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function resolveActionWithAdapters(node, adapters) {
|
|
163
|
-
for (const adapter of adapters) {
|
|
164
|
-
if (!adapter.resolveAction) continue;
|
|
165
|
-
const result = adapter.resolveAction(node);
|
|
166
|
-
if (result) return result;
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
32
|
+
const ADAPTERS = BUILTIN_ADAPTERS;
|
|
170
33
|
|
|
171
34
|
function isIconComponent(name, importSource) {
|
|
172
35
|
if (!name) return false;
|
|
@@ -283,6 +146,12 @@ function classifyActionIntent(text, href) {
|
|
|
283
146
|
}
|
|
284
147
|
|
|
285
148
|
module.exports = {
|
|
149
|
+
AdapterRegistry,
|
|
150
|
+
defaultRegistry,
|
|
151
|
+
BUILTIN_ADAPTERS,
|
|
152
|
+
registerAdapter,
|
|
153
|
+
getAdapter,
|
|
154
|
+
getAllAdapters,
|
|
286
155
|
ADAPTERS,
|
|
287
156
|
activeAdapters,
|
|
288
157
|
recognizeWithAdapters,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const recast = require('recast');
|
|
4
|
+
const b = recast.types.builders;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Canonical Path Abstraction for Deneb ARC.
|
|
8
|
+
* Prevents subtle path drift between runtime siteData getters and visual DOM markers.
|
|
9
|
+
*/
|
|
10
|
+
class CanonicalFieldRef {
|
|
11
|
+
constructor(canonicalPath) {
|
|
12
|
+
if (!canonicalPath || typeof canonicalPath !== 'string') {
|
|
13
|
+
throw new Error(`Invalid canonical path: ${canonicalPath}`);
|
|
14
|
+
}
|
|
15
|
+
this.path = canonicalPath;
|
|
16
|
+
this.parts = canonicalPath.split('.').filter(Boolean);
|
|
17
|
+
this.scope = this.parts[0] || 'home';
|
|
18
|
+
this.section = this.parts.length > 2 ? this.parts[1] : null;
|
|
19
|
+
this.fieldName = this.parts[this.parts.length - 1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds the optional chaining AST expression:
|
|
24
|
+
* siteData?.content?.home?.section?.field
|
|
25
|
+
*/
|
|
26
|
+
toGetterAst() {
|
|
27
|
+
let expr = b.identifier('siteData');
|
|
28
|
+
const allParts = ['content', ...this.parts];
|
|
29
|
+
for (const part of allParts) {
|
|
30
|
+
expr = b.optionalMemberExpression(expr, b.identifier(part), false, true);
|
|
31
|
+
}
|
|
32
|
+
return expr;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the fallback expression:
|
|
37
|
+
* siteData?.content?.home?.section?.field ?? fallback
|
|
38
|
+
*/
|
|
39
|
+
toBindingAst(fallbackValue = '') {
|
|
40
|
+
const getter = this.toGetterAst();
|
|
41
|
+
let fallbackAst;
|
|
42
|
+
if (typeof fallbackValue === 'number') {
|
|
43
|
+
fallbackAst = b.literal(fallbackValue);
|
|
44
|
+
} else if (typeof fallbackValue === 'boolean') {
|
|
45
|
+
fallbackAst = b.literal(fallbackValue);
|
|
46
|
+
} else {
|
|
47
|
+
fallbackAst = b.stringLiteral(String(fallbackValue));
|
|
48
|
+
}
|
|
49
|
+
return b.logicalExpression('??', getter, fallbackAst);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Builds the JSX attribute AST:
|
|
54
|
+
* data-preview-field-path="home.section.field"
|
|
55
|
+
*/
|
|
56
|
+
toPreviewAttrAst() {
|
|
57
|
+
return b.jsxAttribute(
|
|
58
|
+
b.jsxIdentifier('data-preview-field-path'),
|
|
59
|
+
b.stringLiteral(this.path)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generates a nested item path for sub-collections:
|
|
65
|
+
* e.g. parentPath="home.categories[0]", childProp="products", childIndex="2"
|
|
66
|
+
* -> "home.categories[0].products[2]"
|
|
67
|
+
*/
|
|
68
|
+
static buildNestedItemPath(parentItemPath, childProp, childIndexExpr = 'index') {
|
|
69
|
+
return `${parentItemPath}.${childProp}[\${${childIndexExpr}}]`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isValidCanonicalPath(canonicalPath) {
|
|
74
|
+
if (typeof canonicalPath !== 'string' || !canonicalPath.trim()) return false;
|
|
75
|
+
return /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+|\[\d+\]|\[\$\{[^}]+\}\])*$/.test(canonicalPath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validatePathAlignment(getterCode, previewAttrCode) {
|
|
79
|
+
const normalizedGetter = String(getterCode || '').replace(/\?\./g, '.');
|
|
80
|
+
const getterMatch = normalizedGetter.match(/siteData\.content\.([a-zA-Z0-9_.]+)/);
|
|
81
|
+
const attrMatch = String(previewAttrCode || '').match(/data-preview-field-path=["'`{]([a-zA-Z0-9_.]+)/);
|
|
82
|
+
if (!getterMatch || !attrMatch) return false;
|
|
83
|
+
return getterMatch[1] === attrMatch[1];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fieldRef(canonicalPath) {
|
|
87
|
+
return new CanonicalFieldRef(canonicalPath);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
CanonicalFieldRef,
|
|
92
|
+
fieldRef,
|
|
93
|
+
isValidCanonicalPath,
|
|
94
|
+
validatePathAlignment,
|
|
95
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { unwrapExpr } = require('./ast.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Array pipeline methods commonly used before .map()
|
|
7
|
+
*/
|
|
8
|
+
const ARRAY_TRANSFORM_METHODS = new Set([
|
|
9
|
+
'filter',
|
|
10
|
+
'slice',
|
|
11
|
+
'sort',
|
|
12
|
+
'concat',
|
|
13
|
+
'reverse',
|
|
14
|
+
'flatMap',
|
|
15
|
+
'take',
|
|
16
|
+
'splice',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Unrolls a chained array expression to its root data source identifier.
|
|
21
|
+
* e.g.:
|
|
22
|
+
* products.slice(0, 4) -> 'products'
|
|
23
|
+
* products.filter(p => p.featured).slice(0, 4) -> 'products'
|
|
24
|
+
* Object.values(products) -> 'products'
|
|
25
|
+
* [...featured, ...newItems] -> ['featured', 'newItems']
|
|
26
|
+
*/
|
|
27
|
+
function unrollCollectionSource(exprNode, aliasMap = new Map()) {
|
|
28
|
+
if (!exprNode) return null;
|
|
29
|
+
let curr = unwrapExpr(exprNode);
|
|
30
|
+
|
|
31
|
+
// Direct identifier: products
|
|
32
|
+
if (curr.type === 'Identifier') {
|
|
33
|
+
const name = curr.name;
|
|
34
|
+
if (aliasMap.has(name)) {
|
|
35
|
+
return aliasMap.get(name);
|
|
36
|
+
}
|
|
37
|
+
return { rootName: name, chain: [], kind: 'identifier' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Method call: products.slice(0, 4) or products.filter(...).slice(...)
|
|
41
|
+
if (curr.type === 'CallExpression') {
|
|
42
|
+
const callee = curr.callee;
|
|
43
|
+
|
|
44
|
+
// Object.values(products) or Object.entries(products)
|
|
45
|
+
if (
|
|
46
|
+
callee?.type === 'MemberExpression' &&
|
|
47
|
+
callee.object?.type === 'Identifier' &&
|
|
48
|
+
callee.object.name === 'Object' &&
|
|
49
|
+
callee.property?.type === 'Identifier' &&
|
|
50
|
+
(callee.property.name === 'values' || callee.property.name === 'entries')
|
|
51
|
+
) {
|
|
52
|
+
const arg = curr.arguments?.[0];
|
|
53
|
+
if (arg?.type === 'Identifier') {
|
|
54
|
+
const root = unrollCollectionSource(arg, aliasMap);
|
|
55
|
+
return {
|
|
56
|
+
rootName: root?.rootName || arg.name,
|
|
57
|
+
chain: ['Object.' + callee.property.name, ...(root?.chain || [])],
|
|
58
|
+
kind: 'object-values',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// array.method(...)
|
|
64
|
+
if (callee?.type === 'MemberExpression' && !callee.computed && callee.property?.type === 'Identifier') {
|
|
65
|
+
const methodName = callee.property.name;
|
|
66
|
+
if (ARRAY_TRANSFORM_METHODS.has(methodName) || methodName === 'map') {
|
|
67
|
+
const sub = unrollCollectionSource(callee.object, aliasMap);
|
|
68
|
+
if (sub) {
|
|
69
|
+
return {
|
|
70
|
+
rootName: sub.rootName,
|
|
71
|
+
chain: [methodName, ...sub.chain],
|
|
72
|
+
kind: 'pipeline',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Array spread literal: [...featured, ...newItems]
|
|
80
|
+
if (curr.type === 'ArrayExpression') {
|
|
81
|
+
const roots = [];
|
|
82
|
+
for (const el of curr.elements || []) {
|
|
83
|
+
if (el?.type === 'SpreadElement' && el.argument?.type === 'Identifier') {
|
|
84
|
+
const sub = unrollCollectionSource(el.argument, aliasMap);
|
|
85
|
+
roots.push(sub?.rootName || el.argument.name);
|
|
86
|
+
} else if (el?.type === 'Identifier') {
|
|
87
|
+
const sub = unrollCollectionSource(el, aliasMap);
|
|
88
|
+
roots.push(sub?.rootName || el.name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (roots.length > 0) {
|
|
92
|
+
return {
|
|
93
|
+
rootName: roots[0],
|
|
94
|
+
additionalRoots: roots.slice(1),
|
|
95
|
+
chain: ['spread-merge'],
|
|
96
|
+
kind: 'spread',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Collects variable aliases inside an AST that point to arrays.
|
|
106
|
+
* e.g. `const featuredProducts = products.slice(0, 4);`
|
|
107
|
+
*/
|
|
108
|
+
function collectVariableAliases(ast) {
|
|
109
|
+
const aliases = new Map();
|
|
110
|
+
if (!ast) return aliases;
|
|
111
|
+
|
|
112
|
+
const recast = require('recast');
|
|
113
|
+
recast.types.visit(ast, {
|
|
114
|
+
visitVariableDeclarator(pathNode) {
|
|
115
|
+
const node = pathNode.node;
|
|
116
|
+
if (node.id?.type === 'Identifier' && node.init) {
|
|
117
|
+
const varName = node.id.name;
|
|
118
|
+
const resolved = unrollCollectionSource(node.init, aliases);
|
|
119
|
+
if (resolved && (resolved.rootName || resolved.rootIdentifier)) {
|
|
120
|
+
aliases.set(varName, {
|
|
121
|
+
...resolved,
|
|
122
|
+
rootIdentifier: resolved.rootIdentifier || resolved.rootName,
|
|
123
|
+
transformMethod: resolved.chain?.[0] || 'slice',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.traverse(pathNode);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return aliases;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
ARRAY_TRANSFORM_METHODS,
|
|
136
|
+
unrollCollectionSource,
|
|
137
|
+
unrollCollectionPipelines: (ast) => {
|
|
138
|
+
const pipelines = [];
|
|
139
|
+
const recast = require('recast');
|
|
140
|
+
recast.types.visit(ast, {
|
|
141
|
+
visitCallExpression(pathNode) {
|
|
142
|
+
if (pathNode.node.callee?.property?.name === 'map') {
|
|
143
|
+
const res = unrollCollectionSource(pathNode.node.callee.object);
|
|
144
|
+
if (res && res.chain && res.chain.length > 0) {
|
|
145
|
+
pipelines.push({
|
|
146
|
+
rootIdentifier: res.rootName || res.rootIdentifier,
|
|
147
|
+
pipelineChain: [...res.chain].reverse(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.traverse(pathNode);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
return pipelines;
|
|
155
|
+
},
|
|
156
|
+
collectVariableAliases,
|
|
157
|
+
resolveVariableAliases: collectVariableAliases,
|
|
158
|
+
};
|
package/src/arc/diff.cjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { scanProject } = require('./scanner.cjs');
|
|
6
|
+
const { buildProjectIR } = require('./ir-builder.cjs');
|
|
7
|
+
const { analyzeFile } = require('./semantic.cjs');
|
|
8
|
+
const { planTransformations } = require('./planner.cjs');
|
|
9
|
+
const { applyFilePlan } = require('./transformer.cjs');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Computes a unified diff string between original and transformed code.
|
|
13
|
+
*/
|
|
14
|
+
function createUnifiedDiff(filename, oldCode, newCode) {
|
|
15
|
+
const oldLines = oldCode.split('\n');
|
|
16
|
+
const newLines = newCode.split('\n');
|
|
17
|
+
const diffLines = [];
|
|
18
|
+
|
|
19
|
+
diffLines.push(`--- a/${filename}`);
|
|
20
|
+
diffLines.push(`+++ b/${filename}`);
|
|
21
|
+
|
|
22
|
+
let additions = 0;
|
|
23
|
+
let deletions = 0;
|
|
24
|
+
|
|
25
|
+
// Simple chunk comparison for clear visual inspection
|
|
26
|
+
const max = Math.max(oldLines.length, newLines.length);
|
|
27
|
+
for (let i = 0; i < max; i++) {
|
|
28
|
+
const oldLine = oldLines[i];
|
|
29
|
+
const newLine = newLines[i];
|
|
30
|
+
|
|
31
|
+
if (oldLine === newLine) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (oldLine !== undefined && (newLine === undefined || oldLine.trim() !== newLine.trim())) {
|
|
36
|
+
diffLines.push(`- ${oldLine}`);
|
|
37
|
+
deletions++;
|
|
38
|
+
}
|
|
39
|
+
if (newLine !== undefined && (oldLine === undefined || oldLine.trim() !== newLine.trim())) {
|
|
40
|
+
diffLines.push(`+ ${newLine}`);
|
|
41
|
+
additions++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
filename,
|
|
47
|
+
diff: diffLines.join('\n'),
|
|
48
|
+
additions,
|
|
49
|
+
deletions,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Simulates ARC conversion and generates unified diffs for all modified files.
|
|
55
|
+
* Zero files are written to disk.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} targetDirInput - Target project directory.
|
|
58
|
+
* @param {object} options - Options including profile overrides.
|
|
59
|
+
* @returns {Array} Array of file diff objects.
|
|
60
|
+
*/
|
|
61
|
+
function generateArcDiff(targetDirInput = '.', options = {}) {
|
|
62
|
+
const projectDir = path.resolve(targetDirInput);
|
|
63
|
+
const profile = scanProject(projectDir);
|
|
64
|
+
const ir = buildProjectIR(projectDir, profile);
|
|
65
|
+
|
|
66
|
+
const analyses = (profile.jsxFiles || []).map((relativeFile) => {
|
|
67
|
+
const absPath = path.join(projectDir, relativeFile);
|
|
68
|
+
if (!fs.existsSync(absPath)) return null;
|
|
69
|
+
const code = fs.readFileSync(absPath, 'utf8');
|
|
70
|
+
return analyzeFile({
|
|
71
|
+
code,
|
|
72
|
+
relativeFile,
|
|
73
|
+
profile,
|
|
74
|
+
ir,
|
|
75
|
+
});
|
|
76
|
+
}).filter(Boolean);
|
|
77
|
+
|
|
78
|
+
const plan = planTransformations({
|
|
79
|
+
profile,
|
|
80
|
+
analyses,
|
|
81
|
+
ir,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const fileDiffs = [];
|
|
85
|
+
|
|
86
|
+
for (const filePlan of plan.files || []) {
|
|
87
|
+
if (!filePlan.transformations?.length || filePlan.skippedFile) continue;
|
|
88
|
+
let result;
|
|
89
|
+
try {
|
|
90
|
+
result = applyFilePlan(filePlan, profile);
|
|
91
|
+
} catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (result && result.changed && result.code !== filePlan.originalCode) {
|
|
96
|
+
const fileDiff = createUnifiedDiff(filePlan.file, filePlan.originalCode, result.code);
|
|
97
|
+
fileDiffs.push(fileDiff);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return fileDiffs;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pretty-prints file diffs to stdout.
|
|
106
|
+
*/
|
|
107
|
+
function printDiff(fileDiffs) {
|
|
108
|
+
console.log(`\n\x1b[1m\x1b[36mDENEB ARC DIFF\x1b[0m \x1b[90m— Simulated Transformation Diff\x1b[0m`);
|
|
109
|
+
if (!fileDiffs.length) {
|
|
110
|
+
console.log(`\x1b[32m✔ No changes planned or all files already editable.\x1b[0m\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const fd of fileDiffs) {
|
|
115
|
+
console.log(`\n\x1b[1m\x1b[34mDiff: ${fd.filename}\x1b[0m \x1b[90m(+${fd.additions} -${fd.deletions})\x1b[0m`);
|
|
116
|
+
const lines = fd.diff.split('\n');
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (line.startsWith('---') || line.startsWith('+++')) {
|
|
119
|
+
console.log(`\x1b[1m${line}\x1b[0m`);
|
|
120
|
+
} else if (line.startsWith('+')) {
|
|
121
|
+
console.log(`\x1b[32m${line}\x1b[0m`);
|
|
122
|
+
} else if (line.startsWith('-')) {
|
|
123
|
+
console.log(`\x1b[31m${line}\x1b[0m`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(`\x1b[90m${line}\x1b[0m`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
console.log('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
createUnifiedDiff,
|
|
134
|
+
generateArcDiff,
|
|
135
|
+
printDiff,
|
|
136
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { analyzeFile } = require('./semantic.cjs');
|
|
6
|
+
const { scanProject } = require('./scanner.cjs');
|
|
7
|
+
const { buildProjectIR } = require('./ir-builder.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Explains how Deneb ARC analyzes and intends to refactor a specific component file.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} fileInput - Path to the file to explain.
|
|
13
|
+
* @param {object} options - Options including root project directory and json format flag.
|
|
14
|
+
* @returns {object} Analysis explanation summary.
|
|
15
|
+
*/
|
|
16
|
+
function explainFile(fileInput, options = {}) {
|
|
17
|
+
const absPath = path.resolve(options.root || '.', fileInput);
|
|
18
|
+
if (!fs.existsSync(absPath)) {
|
|
19
|
+
throw new Error(`File not found: ${fileInput} (resolved to ${absPath})`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const code = fs.readFileSync(absPath, 'utf8');
|
|
23
|
+
const projectDir = options.root ? path.resolve(options.root) : path.dirname(absPath);
|
|
24
|
+
const relativeFile = path.relative(projectDir, absPath).replace(/\\/g, '/');
|
|
25
|
+
|
|
26
|
+
let profile = options.profile;
|
|
27
|
+
let ir = options.ir;
|
|
28
|
+
if (!profile) {
|
|
29
|
+
try {
|
|
30
|
+
profile = scanProject(projectDir);
|
|
31
|
+
ir = buildProjectIR(projectDir, profile);
|
|
32
|
+
} catch {
|
|
33
|
+
profile = { framework: 'nextjs', root: projectDir };
|
|
34
|
+
ir = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const analysis = analyzeFile({
|
|
39
|
+
code,
|
|
40
|
+
relativeFile,
|
|
41
|
+
profile,
|
|
42
|
+
ir,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const candidates = analysis.candidates || [];
|
|
46
|
+
const editable = candidates.filter((c) => !c.skip);
|
|
47
|
+
const skipped = candidates.filter((c) => c.skip);
|
|
48
|
+
|
|
49
|
+
const explanation = {
|
|
50
|
+
file: relativeFile,
|
|
51
|
+
absolutePath: absPath,
|
|
52
|
+
isTypeScript: /\.(tsx|ts)$/.test(relativeFile),
|
|
53
|
+
alreadyEditable: analysis.alreadyEditable,
|
|
54
|
+
importsCount: Object.keys(analysis.imports || {}).length,
|
|
55
|
+
imports: analysis.imports || {},
|
|
56
|
+
totalCandidates: candidates.length,
|
|
57
|
+
editableCount: editable.length,
|
|
58
|
+
skippedCount: skipped.length,
|
|
59
|
+
editableElements: editable.map((c) => ({
|
|
60
|
+
tag: c.tag,
|
|
61
|
+
kind: c.kind,
|
|
62
|
+
operation: c.operation,
|
|
63
|
+
loc: c.loc,
|
|
64
|
+
value: c.value,
|
|
65
|
+
confidence: c.confidence,
|
|
66
|
+
reason: c.reason,
|
|
67
|
+
proposedField: c.field || `home.${c.kind || 'field'}`,
|
|
68
|
+
})),
|
|
69
|
+
skippedElements: skipped.map((c) => ({
|
|
70
|
+
tag: c.tag,
|
|
71
|
+
kind: c.kind,
|
|
72
|
+
loc: c.loc,
|
|
73
|
+
reason: c.reason,
|
|
74
|
+
confidence: c.confidence,
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return explanation;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pretty-prints the explanation to stdout.
|
|
83
|
+
*/
|
|
84
|
+
function printExplanation(explanation) {
|
|
85
|
+
console.log(`\n\x1b[1m\x1b[36mDENEB ARC EXPLAIN\x1b[0m \x1b[90m— Component Analysis\x1b[0m`);
|
|
86
|
+
console.log(`\x1b[1mFile:\x1b[0m ${explanation.file}`);
|
|
87
|
+
console.log(`\x1b[90mFormat: ${explanation.isTypeScript ? 'TypeScript (TSX)' : 'JavaScript (JSX)'} | Editable: ${explanation.alreadyEditable ? 'Yes' : 'Pending'}\x1b[0m\n`);
|
|
88
|
+
|
|
89
|
+
console.log(`\x1b[1mDiscovered Elements (${explanation.totalCandidates} total, ${explanation.editableCount} editable):\x1b[0m`);
|
|
90
|
+
for (const el of explanation.editableElements) {
|
|
91
|
+
const kindTag = `\x1b[32m[${el.kind.toUpperCase()}]\x1b[0m`;
|
|
92
|
+
const locTag = `\x1b[90m(${el.loc})\x1b[0m`;
|
|
93
|
+
const val = typeof el.value === 'string' ? `"${el.value.slice(0, 40)}${el.value.length > 40 ? '...' : ''}"` : '';
|
|
94
|
+
console.log(` ${kindTag} <${el.tag}> ${locTag} ${val}`);
|
|
95
|
+
console.log(` \x1b[90m↳ Proposed path:\x1b[0m \x1b[33m${el.proposedField}\x1b[0m \x1b[90m(confidence: ${Math.round((el.confidence || 0.8) * 100)}%)\x1b[0m`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (explanation.skippedElements.length > 0) {
|
|
99
|
+
console.log(`\n\x1b[1mPreserved / Skipped Elements (${explanation.skippedElements.length}):\x1b[0m`);
|
|
100
|
+
for (const el of explanation.skippedElements.slice(0, 10)) {
|
|
101
|
+
console.log(` \x1b[90m[PRESERVED] <${el.tag}> (${el.loc}) — reason: ${el.reason}\x1b[0m`);
|
|
102
|
+
}
|
|
103
|
+
if (explanation.skippedElements.length > 10) {
|
|
104
|
+
console.log(` \x1b[90m... and ${explanation.skippedElements.length - 10} more\x1b[0m`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
explainFile,
|
|
112
|
+
printExplanation,
|
|
113
|
+
};
|