@deneb-ui/cli 2.0.88 → 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
|
@@ -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
|
+
};
|