@deneb-ui/cli 2.0.91 → 2.0.92

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.
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+
3
+ const recast = require('recast');
4
+
5
+ const SCOPE_STATUS = {
6
+ ALREADY_BOUND: 'ALREADY_BOUND',
7
+ PROP_BOUND: 'PROP_BOUND',
8
+ HOOK_SAFE: 'HOOK_SAFE',
9
+ SERVER_BOUNDARY: 'SERVER_BOUNDARY',
10
+ UTILITY_FUNCTION: 'UTILITY_FUNCTION',
11
+ PROVIDER_OWNER: 'PROVIDER_OWNER',
12
+ INVALID_SCOPE: 'INVALID_SCOPE',
13
+ UNKNOWN: 'UNKNOWN',
14
+ };
15
+
16
+ /**
17
+ * Checks if an AST node or its return statements return JSX elements or fragments.
18
+ */
19
+ function functionReturnsJsx(fn) {
20
+ if (!fn || !fn.body) return false;
21
+ let returnsJsx = false;
22
+
23
+ // Direct expression body: () => <div />
24
+ if (fn.body.type === 'JSXElement' || fn.body.type === 'JSXFragment') {
25
+ return true;
26
+ }
27
+
28
+ recast.types.visit(fn.body, {
29
+ visitReturnStatement(pathNode) {
30
+ const arg = pathNode.node.argument;
31
+ if (arg && (arg.type === 'JSXElement' || arg.type === 'JSXFragment')) {
32
+ returnsJsx = true;
33
+ return false;
34
+ }
35
+ this.traverse(pathNode);
36
+ },
37
+ // Don't traverse into nested functions to avoid attributing nested JSX to outer function
38
+ visitFunctionDeclaration() {
39
+ return false;
40
+ },
41
+ visitFunctionExpression() {
42
+ return false;
43
+ },
44
+ visitArrowFunctionExpression() {
45
+ return false;
46
+ },
47
+ });
48
+
49
+ return returnsJsx;
50
+ }
51
+
52
+ /**
53
+ * Checks if a function mounts or references <SiteDataProvider> directly in its JSX.
54
+ */
55
+ function functionMountsSiteDataProvider(fn) {
56
+ if (!fn || !fn.body) return false;
57
+ let mounts = false;
58
+
59
+ recast.types.visit(fn.body, {
60
+ visitJSXElement(pathNode) {
61
+ const opening = pathNode.node.openingElement;
62
+ if (opening && opening.name) {
63
+ const name = opening.name.name;
64
+ if (name === 'SiteDataProvider' || name === 'BaseSiteDataProvider' || name === 'DenebDataProvider') {
65
+ mounts = true;
66
+ return false;
67
+ }
68
+ }
69
+ this.traverse(pathNode);
70
+ },
71
+ visitFunctionDeclaration() {
72
+ return false;
73
+ },
74
+ visitFunctionExpression() {
75
+ return false;
76
+ },
77
+ visitArrowFunctionExpression() {
78
+ return false;
79
+ },
80
+ });
81
+
82
+ return mounts;
83
+ }
84
+
85
+ /**
86
+ * Checks if a function has a parameter named siteData (direct or destructured).
87
+ */
88
+ function functionHasSiteDataParam(fn) {
89
+ if (!fn || !Array.isArray(fn.params)) return false;
90
+ for (const param of fn.params) {
91
+ if (param.type === 'Identifier' && param.name === 'siteData') {
92
+ return true;
93
+ }
94
+ if (param.type === 'AssignmentPattern' && param.left && param.left.name === 'siteData') {
95
+ return true;
96
+ }
97
+ if (param.type === 'ObjectPattern' && Array.isArray(param.properties)) {
98
+ for (const prop of param.properties) {
99
+ if (prop.value && prop.value.type === 'Identifier' && prop.value.name === 'siteData') {
100
+ return true;
101
+ }
102
+ if (prop.key && prop.key.type === 'Identifier' && prop.key.name === 'siteData') {
103
+ return true;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Checks if the function body already binds or declares `siteData`.
113
+ */
114
+ function functionLocallyDeclaresSiteData(fn) {
115
+ if (!fn || !fn.body || fn.body.type !== 'BlockStatement') return false;
116
+ let declares = false;
117
+
118
+ for (const stmt of fn.body.body) {
119
+ if (stmt.type === 'VariableDeclaration') {
120
+ for (const decl of stmt.declarations) {
121
+ if (decl.id && decl.id.type === 'Identifier' && decl.id.name === 'siteData') {
122
+ declares = true;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ if (declares) break;
128
+ }
129
+
130
+ return declares;
131
+ }
132
+
133
+ /**
134
+ * Checks if function calls useSiteData() already.
135
+ */
136
+ function functionCallsUseSiteData(fn) {
137
+ if (!fn || !fn.body) return false;
138
+ let calls = false;
139
+
140
+ recast.types.visit(fn.body, {
141
+ visitCallExpression(pathNode) {
142
+ const callee = pathNode.node.callee;
143
+ if (callee && callee.type === 'Identifier' && callee.name === 'useSiteData') {
144
+ calls = true;
145
+ return false;
146
+ }
147
+ this.traverse(pathNode);
148
+ },
149
+ });
150
+
151
+ return calls;
152
+ }
153
+
154
+ /**
155
+ * Checks if a function is a React component or custom hook.
156
+ */
157
+ function isReactComponentOrHook(fn, fnName) {
158
+ // Custom hooks: use*
159
+ if (fnName && /^use[A-Z]/.test(fnName)) return true;
160
+
161
+ // Capitalized function name
162
+ const isCapitalized = Boolean(fnName && /^[A-Z]/.test(fnName));
163
+
164
+ // Must return JSX to qualify as a component if not a hook
165
+ const returnsJsx = functionReturnsJsx(fn);
166
+
167
+ if (isCapitalized && returnsJsx) return true;
168
+ if (returnsJsx) return true; // Anonymous or exported default component returning JSX
169
+
170
+ return false;
171
+ }
172
+
173
+ /**
174
+ * Classifies whether a function within an AST can safely have `useSiteData()` injected.
175
+ *
176
+ * @param {object} fn AST Function node
177
+ * @param {string|null} fnName Identifier name of function if available
178
+ * @param {object} fileContext Context about the file (isServerComponent, mountsProvider, filePath)
179
+ * @returns {string} Status code from SCOPE_STATUS
180
+ */
181
+ function classifySiteDataRequirement(fn, fnName, fileContext = {}) {
182
+ if (!fn) return SCOPE_STATUS.INVALID_SCOPE;
183
+
184
+ // 1. Check if function already declares or receives siteData
185
+ if (functionHasSiteDataParam(fn)) {
186
+ return SCOPE_STATUS.PROP_BOUND;
187
+ }
188
+
189
+ if (functionLocallyDeclaresSiteData(fn) || functionCallsUseSiteData(fn)) {
190
+ return SCOPE_STATUS.ALREADY_BOUND;
191
+ }
192
+
193
+ // 2. Check if async function -> Server Component / async handler
194
+ if (fn.async) {
195
+ return SCOPE_STATUS.SERVER_BOUNDARY;
196
+ }
197
+
198
+ // 3. Check if function mounts <SiteDataProvider>
199
+ if (functionMountsSiteDataProvider(fn) || fileContext.mountsProvider) {
200
+ return SCOPE_STATUS.PROVIDER_OWNER;
201
+ }
202
+
203
+ // 4. Check if function is RootLayout or Server Layout without 'use client'
204
+ const isLayout = /(?:^|[/\\])layout\.(?:tsx|jsx|js|ts)$/.test(fileContext.filePath || '');
205
+ if (fnName === 'RootLayout' || (isLayout && !fileContext.isClientComponent)) {
206
+ return SCOPE_STATUS.SERVER_BOUNDARY;
207
+ }
208
+
209
+ // 5. Check if React component or custom hook
210
+ if (!isReactComponentOrHook(fn, fnName)) {
211
+ return SCOPE_STATUS.UTILITY_FUNCTION;
212
+ }
213
+
214
+ // 6. Safe to inject hook
215
+ return SCOPE_STATUS.HOOK_SAFE;
216
+ }
217
+
218
+ module.exports = {
219
+ SCOPE_STATUS,
220
+ functionReturnsJsx,
221
+ functionMountsSiteDataProvider,
222
+ functionHasSiteDataParam,
223
+ functionLocallyDeclaresSiteData,
224
+ functionCallsUseSiteData,
225
+ isReactComponentOrHook,
226
+ classifySiteDataRequirement,
227
+ };