@deneb-ui/cli 2.0.91 → 2.0.93
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/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +38 -1
- package/src/arc/__tests__/composite-fragments.test.cjs +48 -0
- package/src/arc/__tests__/fifteen-gates.test.cjs +47 -0
- package/src/arc/__tests__/fivora-schema-authority.test.cjs +75 -0
- package/src/arc/__tests__/recovery-and-inventory.test.cjs +65 -0
- package/src/arc/__tests__/routing-and-assets.test.cjs +54 -0
- package/src/arc/__tests__/site-data-scope.test.cjs +94 -0
- package/src/arc/__tests__/transform-proof.test.cjs +43 -0
- package/src/arc/__tests__/vanta-regressions.test.cjs +110 -0
- package/src/arc/acceptance-gates.cjs +21 -3
- package/src/arc/asset-auditor.cjs +115 -0
- package/src/arc/confidence-recovery.cjs +87 -0
- package/src/arc/editability-inventory.cjs +84 -0
- package/src/arc/fivora-schema-authority.cjs +203 -0
- package/src/arc/index.cjs +90 -4
- package/src/arc/manifest.cjs +16 -4
- package/src/arc/planner.cjs +13 -1
- package/src/arc/transform-proof.cjs +111 -0
- package/src/arc/transforms/healing/sanitizer.cjs +29 -2
- package/src/arc/transforms/primitives/text.cjs +18 -2
- package/src/arc/transforms/routing/page-key.cjs +43 -24
- package/src/arc/transforms/runtime/provider.cjs +51 -21
- package/src/arc/transforms/runtime/site-data-scope.cjs +227 -0
- package/src/sites/universal-page-selection.ts +25 -0
- package/src/tools/deneb-template-validator.cjs +19 -18
package/src/arc/manifest.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const { readJsonSafe, writeJson, deepMerge, isPlainObject } = require('./fs-utils.cjs');
|
|
5
6
|
const { ARC_VERSION, SCHEMA_VERSION } = require('./version.cjs');
|
|
@@ -12,6 +13,7 @@ const {
|
|
|
12
13
|
canonicalizeMarkerPath,
|
|
13
14
|
wildcardPath,
|
|
14
15
|
} = require('./fivora-contract.cjs');
|
|
16
|
+
const { sanitizeEditorSections } = require('./fivora-schema-authority.cjs');
|
|
15
17
|
|
|
16
18
|
function pruneUnboundLeaves(content, isBound) {
|
|
17
19
|
function walk(node, path) {
|
|
@@ -322,14 +324,24 @@ function collectStylesFromPlan(plan, existingStyles) {
|
|
|
322
324
|
return styles;
|
|
323
325
|
}
|
|
324
326
|
|
|
325
|
-
function baseContent(projectName, routes) {
|
|
327
|
+
function baseContent(projectName, routes, projectDir) {
|
|
326
328
|
const navLabels = {};
|
|
327
329
|
for (const page of routes) navLabels[page.id] = page.label || page.id;
|
|
330
|
+
let logoUrl = '';
|
|
331
|
+
if (projectDir) {
|
|
332
|
+
const candidates = ['logo.svg', 'logo.png', 'logo-dark.svg', 'icon.svg', 'icon.png', 'fivora-logo.png'];
|
|
333
|
+
for (const cand of candidates) {
|
|
334
|
+
if (fs.existsSync(path.join(projectDir, 'public', cand))) {
|
|
335
|
+
logoUrl = `/${cand}`;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
328
340
|
return {
|
|
329
341
|
common: {
|
|
330
342
|
websiteTitle: projectName,
|
|
331
343
|
shortDescription: `A high-converting storefront built for the Fivora platform.`,
|
|
332
|
-
logoUrl
|
|
344
|
+
logoUrl,
|
|
333
345
|
headerCtaLabel: 'Contact Us',
|
|
334
346
|
copyright: `© ${new Date().getFullYear()} ${projectName}. All rights reserved.`,
|
|
335
347
|
navLabels,
|
|
@@ -519,7 +531,7 @@ function buildSiteDataAndManifest({
|
|
|
519
531
|
...(r.required ? { required: true } : {}),
|
|
520
532
|
}));
|
|
521
533
|
|
|
522
|
-
const content = baseContent(projectName, routes);
|
|
534
|
+
const content = baseContent(projectName, routes, projectDir);
|
|
523
535
|
|
|
524
536
|
// Recipes may hint schema shape, but must not dump another storefront's content
|
|
525
537
|
// into an unrelated project. Extracted values always win.
|
|
@@ -678,7 +690,7 @@ function buildSiteDataAndManifest({
|
|
|
678
690
|
pages: routes,
|
|
679
691
|
editorSchema: {
|
|
680
692
|
version: 1,
|
|
681
|
-
sections: editorSections,
|
|
693
|
+
sections: sanitizeEditorSections(editorSections),
|
|
682
694
|
},
|
|
683
695
|
};
|
|
684
696
|
|
package/src/arc/planner.cjs
CHANGED
|
@@ -6,6 +6,7 @@ const { classifyHref } = require('./adapters.cjs');
|
|
|
6
6
|
const { loadFingerprintBoost } = require('./learning.cjs');
|
|
7
7
|
const { appendStyleBindTransforms } = require('./style-candidates.cjs');
|
|
8
8
|
const { classifyDataCandidate, isEditableClassification } = require('./data-classification.cjs');
|
|
9
|
+
const { recoverCandidateConfidence } = require('./confidence-recovery.cjs');
|
|
9
10
|
|
|
10
11
|
function recipeBoost(candidate, recipe) {
|
|
11
12
|
if (!recipe) return 0;
|
|
@@ -44,10 +45,21 @@ function planTransformations({ profile, analyses, recipe, ir }) {
|
|
|
44
45
|
});
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
let confidence = Math.min(
|
|
48
49
|
0.99,
|
|
49
50
|
(candidate.confidence || 0) + recipeBoost(candidate, recipe) + (fingerprintHint.boost || 0),
|
|
50
51
|
);
|
|
52
|
+
|
|
53
|
+
// Multi-stage recovery pass for low-confidence candidates (< 0.60)
|
|
54
|
+
let recovered = false;
|
|
55
|
+
if (confidence < CONFIDENCE.VALIDATE && !candidate.skip) {
|
|
56
|
+
const rec = recoverCandidateConfidence(candidate);
|
|
57
|
+
if (rec.recovered) {
|
|
58
|
+
confidence = rec.confidence;
|
|
59
|
+
recovered = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
const dataClass = classifyDataCandidate(candidate, {
|
|
52
64
|
file: candidate.file,
|
|
53
65
|
componentName: candidate.componentName,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TransformProof (P8, P9, P10)
|
|
5
|
+
*
|
|
6
|
+
* Formal evidence record for every AST mutation executed by ARC.
|
|
7
|
+
* Captures source provenance, target schema paths, preconditions,
|
|
8
|
+
* and multi-stage verification proofs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class TransformProof {
|
|
12
|
+
constructor({
|
|
13
|
+
transformId,
|
|
14
|
+
type,
|
|
15
|
+
source = {},
|
|
16
|
+
target = {},
|
|
17
|
+
preconditions = [],
|
|
18
|
+
verification = {},
|
|
19
|
+
}) {
|
|
20
|
+
this.transformId = transformId || `tr_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
21
|
+
this.type = type; // 'TEXT_BIND' | 'ACTION_SPLIT' | 'COLLECTION_BIND' | 'IMAGE_BIND' | 'STYLE_BIND'
|
|
22
|
+
this.source = {
|
|
23
|
+
file: source.file || '',
|
|
24
|
+
loc: source.loc || '',
|
|
25
|
+
tag: source.tag || '',
|
|
26
|
+
originalText: source.originalText || '',
|
|
27
|
+
};
|
|
28
|
+
this.target = {
|
|
29
|
+
path: target.path || '',
|
|
30
|
+
type: target.type || 'text',
|
|
31
|
+
section: target.section || '',
|
|
32
|
+
};
|
|
33
|
+
this.preconditions = Array.isArray(preconditions) ? preconditions : [];
|
|
34
|
+
this.verification = {
|
|
35
|
+
astParsed: Boolean(verification.astParsed ?? true),
|
|
36
|
+
contractValid: Boolean(verification.contractValid ?? true),
|
|
37
|
+
runtimeTestable: Boolean(verification.runtimeTestable ?? true),
|
|
38
|
+
designPreserved: Boolean(verification.designPreserved ?? true),
|
|
39
|
+
};
|
|
40
|
+
this.timestamp = Date.now();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isVerified() {
|
|
44
|
+
return (
|
|
45
|
+
this.verification.astParsed &&
|
|
46
|
+
this.verification.contractValid &&
|
|
47
|
+
this.verification.designPreserved
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toJSON() {
|
|
52
|
+
return {
|
|
53
|
+
transformId: this.transformId,
|
|
54
|
+
type: this.type,
|
|
55
|
+
source: this.source,
|
|
56
|
+
target: this.target,
|
|
57
|
+
preconditions: this.preconditions,
|
|
58
|
+
verification: this.verification,
|
|
59
|
+
timestamp: this.timestamp,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class ProofRegistry {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.proofs = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
record(proofData) {
|
|
70
|
+
const proof = new TransformProof(proofData);
|
|
71
|
+
this.proofs.push(proof);
|
|
72
|
+
return proof;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getAll() {
|
|
76
|
+
return [...this.proofs];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getVerified() {
|
|
80
|
+
return this.proofs.filter((p) => p.isVerified());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
verifyPhaseInvariants({ plannedCount, boundCount, schemaPathCount }) {
|
|
84
|
+
const violations = [];
|
|
85
|
+
|
|
86
|
+
// Invariant 1: Every planned transformation must emit a proof
|
|
87
|
+
if (this.proofs.length < plannedCount) {
|
|
88
|
+
violations.push(`Phase Invariant Violation: Planned ${plannedCount} transforms, but only emitted ${this.proofs.length} proofs.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Invariant 2: Every verified proof must have a non-empty target path
|
|
92
|
+
const emptyTargets = this.proofs.filter((p) => !p.target.path);
|
|
93
|
+
if (emptyTargets.length > 0) {
|
|
94
|
+
violations.push(`Phase Invariant Violation: Found ${emptyTargets.length} proofs with empty target paths.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
passed: violations.length === 0,
|
|
99
|
+
violations,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
toJSON() {
|
|
104
|
+
return this.proofs.map((p) => (typeof p.toJSON === 'function' ? p.toJSON() : p));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
TransformProof,
|
|
110
|
+
ProofRegistry,
|
|
111
|
+
};
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
} = require('../../ast.cjs');
|
|
13
13
|
const { BROAD_CONTENT_CONTAINERS } = require('../../fivora-contract.cjs');
|
|
14
14
|
const { injectSiteDataHook } = require('../runtime/provider.cjs');
|
|
15
|
+
const { ensureClientDirective } = require('../../rsc-boundary.cjs');
|
|
15
16
|
|
|
16
17
|
function sanitizeContradictoryMarkers(ast) {
|
|
17
18
|
let cleaned = 0;
|
|
@@ -378,11 +379,37 @@ function healDecorativeOverlays(ast) {
|
|
|
378
379
|
|
|
379
380
|
function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
|
|
380
381
|
if (!code.includes('siteData')) return code;
|
|
382
|
+
|
|
383
|
+
// Never touch files mounting SiteDataProvider
|
|
384
|
+
if (code.includes('SiteDataProvider')) return code;
|
|
385
|
+
|
|
386
|
+
// Never touch Server Component layouts or metadata files
|
|
387
|
+
const isLayout = /(?:^|[/\\])layout\.(?:tsx|jsx|js|ts)$/.test(filePath);
|
|
388
|
+
const isClient = /['"]use client['"]/.test(code);
|
|
389
|
+
if (isLayout && !isClient) return code;
|
|
390
|
+
|
|
391
|
+
// Never touch plain utilities or config files that contain zero JSX
|
|
392
|
+
const hasJsx = /<[a-zA-Z][\s\S]*?>/.test(code);
|
|
393
|
+
const isHookFile = /(?:^|[/\\])use[A-Z]/.test(filePath);
|
|
394
|
+
if (!hasJsx && !isHookFile) return code;
|
|
395
|
+
|
|
381
396
|
try {
|
|
382
397
|
const ast = parseSource(code, filePath);
|
|
383
|
-
const
|
|
398
|
+
const fileContext = {
|
|
399
|
+
filePath,
|
|
400
|
+
isServerComponent: !isClient,
|
|
401
|
+
isClientComponent: isClient,
|
|
402
|
+
mountsProvider: code.includes('SiteDataProvider'),
|
|
403
|
+
};
|
|
404
|
+
const injected = injectSiteDataHook(ast, filePath, fileContext);
|
|
384
405
|
if (injected) {
|
|
385
|
-
|
|
406
|
+
if (!isClient) {
|
|
407
|
+
ensureClientDirective(ast);
|
|
408
|
+
}
|
|
409
|
+
const importSpecifier = code.includes('@/lib/siteDataContext')
|
|
410
|
+
? '@/lib/siteDataContext'
|
|
411
|
+
: '@deneb-ui/ui';
|
|
412
|
+
ensureImport(ast, importSpecifier, ['useSiteData']);
|
|
386
413
|
return printSource(ast, code);
|
|
387
414
|
}
|
|
388
415
|
} catch {}
|
|
@@ -7,9 +7,25 @@ const {
|
|
|
7
7
|
} = require('../../ast.cjs');
|
|
8
8
|
|
|
9
9
|
function replaceTextChildren(node, fieldPath, fallback, fieldType) {
|
|
10
|
+
const children = node.children || [];
|
|
11
|
+
const hasDynamicExpr = children.some(
|
|
12
|
+
(c) =>
|
|
13
|
+
c &&
|
|
14
|
+
c.type === 'JSXExpressionContainer' &&
|
|
15
|
+
c.expression &&
|
|
16
|
+
c.expression.type !== 'StringLiteral' &&
|
|
17
|
+
c.expression.type !== 'Literal'
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// If there are dynamic runtime expressions, preserve them and wrap the static literal text
|
|
21
|
+
if (hasDynamicExpr) {
|
|
22
|
+
wrapLiteralTextChildren(node, fieldPath, fallback);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
const nextChildren = [];
|
|
11
27
|
let replaced = false;
|
|
12
|
-
for (const child of
|
|
28
|
+
for (const child of children) {
|
|
13
29
|
if (!child) continue;
|
|
14
30
|
if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
|
|
15
31
|
nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
|
|
@@ -35,7 +51,7 @@ function replaceTextChildren(node, fieldPath, fallback, fieldType) {
|
|
|
35
51
|
|
|
36
52
|
/**
|
|
37
53
|
* Replaces literal text children with an editable <span>, leaving the container
|
|
38
|
-
* element and
|
|
54
|
+
* element, icons, and dynamic expressions untouched.
|
|
39
55
|
*/
|
|
40
56
|
function wrapLiteralTextChildren(node, fieldPath, fallback) {
|
|
41
57
|
const nextChildren = [];
|
|
@@ -27,11 +27,9 @@ function inferPageKey(relativeFile) {
|
|
|
27
27
|
* rather than being guessed from the filename — Pages Router uses
|
|
28
28
|
* `pages/contact.jsx`, App Router uses `app/contact/page.tsx`.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
* the outermost element the page component returns.
|
|
30
|
+
* <main> is preferred, then any host element (div/section), then any root component element.
|
|
32
31
|
*/
|
|
33
32
|
function instrumentPageKey(code, relativeFile, pageKey) {
|
|
34
|
-
if (code.includes('data-preview-page-key')) return { code, updated: false };
|
|
35
33
|
const key = pageKey || inferPageKey(relativeFile);
|
|
36
34
|
|
|
37
35
|
let ast;
|
|
@@ -41,40 +39,61 @@ function instrumentPageKey(code, relativeFile, pageKey) {
|
|
|
41
39
|
return { code, updated: false };
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
b.jsxAttribute(b.jsxIdentifier('data-preview-page-key'), b.stringLiteral(key));
|
|
46
|
-
|
|
42
|
+
let existingAttr = null;
|
|
47
43
|
let target = null;
|
|
44
|
+
let fallbackTarget = null;
|
|
45
|
+
|
|
48
46
|
recast.types.visit(ast, {
|
|
49
47
|
visitJSXOpeningElement(pathNode) {
|
|
48
|
+
const attrs = pathNode.node.attributes || [];
|
|
49
|
+
const pageKeyAttr = attrs.find(
|
|
50
|
+
(a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-page-key'
|
|
51
|
+
);
|
|
52
|
+
if (pageKeyAttr) {
|
|
53
|
+
existingAttr = pageKeyAttr;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
const name = pathNode.node.name && pathNode.node.name.name;
|
|
51
58
|
if (!target && name === 'main') {
|
|
52
59
|
target = pathNode.node;
|
|
53
|
-
|
|
60
|
+
} else if (!fallbackTarget && typeof name === 'string' && /^[a-z]/.test(name)) {
|
|
61
|
+
fallbackTarget = pathNode.node;
|
|
62
|
+
} else if (!fallbackTarget) {
|
|
63
|
+
// Root custom component (e.g. <PlatformProductDetail />)
|
|
64
|
+
fallbackTarget = pathNode.node;
|
|
54
65
|
}
|
|
66
|
+
|
|
55
67
|
this.traverse(pathNode);
|
|
56
68
|
},
|
|
57
69
|
});
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// If already has data-preview-page-key, ensure its value matches the expected page key
|
|
72
|
+
if (existingAttr) {
|
|
73
|
+
let currentValue = null;
|
|
74
|
+
if (existingAttr.value) {
|
|
75
|
+
if (existingAttr.value.type === 'StringLiteral') {
|
|
76
|
+
currentValue = existingAttr.value.value;
|
|
77
|
+
} else if (existingAttr.value.type === 'JSXExpressionContainer' && existingAttr.value.expression.type === 'StringLiteral') {
|
|
78
|
+
currentValue = existingAttr.value.expression.value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (currentValue === key) {
|
|
82
|
+
return { code, updated: false };
|
|
83
|
+
}
|
|
84
|
+
// Update to correct key
|
|
85
|
+
existingAttr.value = b.stringLiteral(key);
|
|
86
|
+
return { code: printSource(ast, code), updated: true };
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
const finalTarget = target || fallbackTarget;
|
|
90
|
+
if (!finalTarget) return { code, updated: false };
|
|
91
|
+
|
|
92
|
+
finalTarget.attributes = finalTarget.attributes || [];
|
|
93
|
+
finalTarget.attributes.unshift(
|
|
94
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-page-key'), b.stringLiteral(key))
|
|
95
|
+
);
|
|
96
|
+
|
|
78
97
|
return { code: printSource(ast, code), updated: true };
|
|
79
98
|
}
|
|
80
99
|
|
|
@@ -77,11 +77,24 @@ function functionReferencesSiteData(fn) {
|
|
|
77
77
|
return found;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
const {
|
|
81
|
+
SCOPE_STATUS,
|
|
82
|
+
classifySiteDataRequirement,
|
|
83
|
+
} = require('./site-data-scope.cjs');
|
|
84
|
+
|
|
85
|
+
function injectSiteDataHook(ast, filePath = 'file.tsx', options = {}) {
|
|
81
86
|
let injected = false;
|
|
82
87
|
let anyFunctionReferencedSiteData = false;
|
|
83
88
|
let alreadyHasUseSiteData = false;
|
|
84
89
|
|
|
90
|
+
const fileContext = {
|
|
91
|
+
filePath,
|
|
92
|
+
isServerComponent: options.isServerComponent ?? false,
|
|
93
|
+
isClientComponent: options.isClientComponent ?? false,
|
|
94
|
+
mountsProvider: options.mountsProvider ?? false,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Inspect file-level AST properties
|
|
85
98
|
recast.types.visit(ast, {
|
|
86
99
|
visitCallExpression(pathNode) {
|
|
87
100
|
if (pathNode.node.callee && pathNode.node.callee.name === 'useSiteData') {
|
|
@@ -90,13 +103,35 @@ function injectSiteDataHook(ast) {
|
|
|
90
103
|
}
|
|
91
104
|
this.traverse(pathNode);
|
|
92
105
|
},
|
|
106
|
+
visitJSXElement(pathNode) {
|
|
107
|
+
const opening = pathNode.node.openingElement;
|
|
108
|
+
if (opening && opening.name) {
|
|
109
|
+
const name = opening.name.name;
|
|
110
|
+
if (name === 'SiteDataProvider' || name === 'BaseSiteDataProvider' || name === 'DenebDataProvider') {
|
|
111
|
+
fileContext.mountsProvider = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.traverse(pathNode);
|
|
115
|
+
},
|
|
93
116
|
});
|
|
94
117
|
|
|
95
|
-
|
|
118
|
+
// If this file mounts SiteDataProvider, consumer hook injection is forbidden
|
|
119
|
+
if (fileContext.mountsProvider) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function injectIntoFunction(fn, fnName) {
|
|
96
124
|
if (!fn || !fn.body || fn.body.type !== 'BlockStatement') return false;
|
|
125
|
+
|
|
126
|
+
const requirement = classifySiteDataRequirement(fn, fnName, fileContext);
|
|
127
|
+
if (requirement !== SCOPE_STATUS.HOOK_SAFE) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
97
131
|
const body = fn.body.body;
|
|
98
132
|
const already = body.some((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
99
133
|
if (already) return false;
|
|
134
|
+
|
|
100
135
|
const hook = b.variableDeclaration('const', [
|
|
101
136
|
b.variableDeclarator(
|
|
102
137
|
b.identifier('siteData'),
|
|
@@ -108,40 +143,45 @@ function injectSiteDataHook(ast) {
|
|
|
108
143
|
return true;
|
|
109
144
|
}
|
|
110
145
|
|
|
111
|
-
// Pass 1: Inject useSiteData() into
|
|
146
|
+
// Pass 1: Inject useSiteData() into component functions that reference siteData
|
|
112
147
|
recast.types.visit(ast, {
|
|
113
148
|
visitFunctionDeclaration(pathNode) {
|
|
149
|
+
const name = pathNode.node.id ? pathNode.node.id.name : null;
|
|
114
150
|
if (functionReferencesSiteData(pathNode.node)) {
|
|
115
151
|
anyFunctionReferencedSiteData = true;
|
|
116
|
-
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
152
|
+
if (injectIntoFunction(pathNode.node, name)) injected = true;
|
|
117
153
|
}
|
|
118
154
|
this.traverse(pathNode);
|
|
119
155
|
},
|
|
120
156
|
visitFunctionExpression(pathNode) {
|
|
157
|
+
const parent = pathNode.parent && pathNode.parent.node;
|
|
158
|
+
const name = parent && parent.type === 'VariableDeclarator' && parent.id ? parent.id.name : null;
|
|
121
159
|
if (functionReferencesSiteData(pathNode.node)) {
|
|
122
160
|
anyFunctionReferencedSiteData = true;
|
|
123
|
-
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
161
|
+
if (injectIntoFunction(pathNode.node, name)) injected = true;
|
|
124
162
|
}
|
|
125
163
|
this.traverse(pathNode);
|
|
126
164
|
},
|
|
127
165
|
visitArrowFunctionExpression(pathNode) {
|
|
166
|
+
const parent = pathNode.parent && pathNode.parent.node;
|
|
167
|
+
const name = parent && parent.type === 'VariableDeclarator' && parent.id ? parent.id.name : null;
|
|
128
168
|
if (functionReferencesSiteData(pathNode.node)) {
|
|
129
169
|
anyFunctionReferencedSiteData = true;
|
|
130
|
-
if (injectIntoFunction(pathNode.node)) injected = true;
|
|
170
|
+
if (injectIntoFunction(pathNode.node, name)) injected = true;
|
|
131
171
|
}
|
|
132
172
|
this.traverse(pathNode);
|
|
133
173
|
},
|
|
134
174
|
});
|
|
135
175
|
|
|
136
|
-
// Pass 2: If no component references siteData yet
|
|
137
|
-
// ensure the primary component receives useSiteData()
|
|
176
|
+
// Pass 2: If no component references siteData yet, ensure the primary component receives useSiteData()
|
|
138
177
|
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
139
178
|
recast.types.visit(ast, {
|
|
140
179
|
visitExportDefaultDeclaration(pathNode) {
|
|
141
180
|
if (injected) return false;
|
|
142
181
|
const decl = pathNode.node.declaration;
|
|
143
182
|
if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
144
|
-
|
|
183
|
+
const name = decl.id ? decl.id.name : 'DefaultExport';
|
|
184
|
+
injected = injectIntoFunction(decl, name);
|
|
145
185
|
}
|
|
146
186
|
this.traverse(pathNode);
|
|
147
187
|
},
|
|
@@ -149,7 +189,7 @@ function injectSiteDataHook(ast) {
|
|
|
149
189
|
if (injected) return false;
|
|
150
190
|
const name = pathNode.node.id && pathNode.node.id.name;
|
|
151
191
|
if (name && /^[A-Z]/.test(name)) {
|
|
152
|
-
injected = injectIntoFunction(pathNode.node);
|
|
192
|
+
injected = injectIntoFunction(pathNode.node, name);
|
|
153
193
|
return false;
|
|
154
194
|
}
|
|
155
195
|
this.traverse(pathNode);
|
|
@@ -159,7 +199,7 @@ function injectSiteDataHook(ast) {
|
|
|
159
199
|
const id = pathNode.node.id;
|
|
160
200
|
const init = pathNode.node.init;
|
|
161
201
|
if (id && id.type === 'Identifier' && /^[A-Z]/.test(id.name) && init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
162
|
-
injected = injectIntoFunction(init);
|
|
202
|
+
injected = injectIntoFunction(init, id.name);
|
|
163
203
|
return false;
|
|
164
204
|
}
|
|
165
205
|
this.traverse(pathNode);
|
|
@@ -167,16 +207,6 @@ function injectSiteDataHook(ast) {
|
|
|
167
207
|
});
|
|
168
208
|
}
|
|
169
209
|
|
|
170
|
-
if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
|
|
171
|
-
recast.types.visit(ast, {
|
|
172
|
-
visitFunctionDeclaration(pathNode) {
|
|
173
|
-
if (injected) return false;
|
|
174
|
-
injected = injectIntoFunction(pathNode.node);
|
|
175
|
-
return false;
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
210
|
return injected;
|
|
181
211
|
}
|
|
182
212
|
|