@l4yercak3/cli 1.2.0 → 1.2.2
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/docs/ADDING_FRAMEWORK_DETECTORS.md +391 -0
- package/package.json +1 -1
- package/src/commands/spread.js +27 -12
- package/src/detectors/expo-detector.js +166 -0
- package/src/detectors/registry.js +2 -0
- package/src/generators/api-client-generator.js +22 -7
- package/src/generators/index.js +33 -3
- package/tests/expo-detector.test.js +264 -0
- package/tests/generators-index.test.js +21 -2
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# Adding New Framework Detectors
|
|
2
|
+
|
|
3
|
+
This guide explains how to add support for new frameworks/platforms to the L4YERCAK3 CLI.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The CLI uses a detector system to identify what type of project it's running in. Each detector:
|
|
8
|
+
1. Checks if the project matches its framework
|
|
9
|
+
2. Returns metadata about the project (version, router type, TypeScript, etc.)
|
|
10
|
+
3. Specifies which features and generators are supported
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/detectors/
|
|
16
|
+
├── base-detector.js # Base class all detectors extend
|
|
17
|
+
├── registry.js # Central registry that runs all detectors
|
|
18
|
+
├── index.js # Main entry point (orchestrates detection)
|
|
19
|
+
├── nextjs-detector.js # Next.js detector
|
|
20
|
+
├── expo-detector.js # Expo/React Native detector
|
|
21
|
+
└── [your-detector].js # Your new detector
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Step 1: Create the Detector File
|
|
25
|
+
|
|
26
|
+
Create a new file in `src/detectors/` named `{framework}-detector.js`.
|
|
27
|
+
|
|
28
|
+
### Template
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
/**
|
|
32
|
+
* {Framework} Project Detector
|
|
33
|
+
* Detects {Framework} projects and their configuration
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const BaseDetector = require('./base-detector');
|
|
39
|
+
|
|
40
|
+
class {Framework}Detector extends BaseDetector {
|
|
41
|
+
/**
|
|
42
|
+
* Unique identifier for this framework
|
|
43
|
+
* Used in registration data and throughout the CLI
|
|
44
|
+
*/
|
|
45
|
+
get name() {
|
|
46
|
+
return '{framework}'; // lowercase, e.g., 'remix', 'astro', 'sveltekit'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Detection priority (0-100)
|
|
51
|
+
*
|
|
52
|
+
* Guidelines:
|
|
53
|
+
* - 100: Meta-frameworks (Next.js, Nuxt, SvelteKit)
|
|
54
|
+
* - 95: Platform-specific (Expo, Tauri)
|
|
55
|
+
* - 75: Framework + bundler (Vite + React)
|
|
56
|
+
* - 50: Pure frameworks (React, Vue)
|
|
57
|
+
* - 25: Generic projects
|
|
58
|
+
*/
|
|
59
|
+
get priority() {
|
|
60
|
+
return 90; // Adjust based on specificity
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Detect if this is a {Framework} project
|
|
65
|
+
*
|
|
66
|
+
* @param {string} projectPath - Directory to check
|
|
67
|
+
* @returns {object} Detection result
|
|
68
|
+
*/
|
|
69
|
+
detect(projectPath = process.cwd()) {
|
|
70
|
+
// Check for package.json
|
|
71
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
72
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
73
|
+
return { detected: false, confidence: 0, metadata: {} };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
78
|
+
const dependencies = {
|
|
79
|
+
...packageJson.dependencies,
|
|
80
|
+
...packageJson.devDependencies,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Check for framework-specific dependency
|
|
84
|
+
if (!dependencies['{framework-package}']) {
|
|
85
|
+
return { detected: false, confidence: 0, metadata: {} };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Gather metadata
|
|
89
|
+
const metadata = {
|
|
90
|
+
version: dependencies['{framework-package}'],
|
|
91
|
+
hasTypeScript: !!dependencies.typescript ||
|
|
92
|
+
fs.existsSync(path.join(projectPath, 'tsconfig.json')),
|
|
93
|
+
// Add framework-specific metadata:
|
|
94
|
+
routerType: this.detectRouterType(dependencies),
|
|
95
|
+
// config, plugins, etc.
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
detected: true,
|
|
100
|
+
confidence: 0.95, // Adjust based on detection certainty
|
|
101
|
+
metadata,
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return { detected: false, confidence: 0, metadata: {} };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Detect router type for this framework
|
|
110
|
+
* IMPORTANT: Always return a string, never null/undefined
|
|
111
|
+
*/
|
|
112
|
+
detectRouterType(dependencies) {
|
|
113
|
+
// Example for a framework with multiple router options
|
|
114
|
+
if (dependencies['{file-router-package}']) {
|
|
115
|
+
return 'file-based';
|
|
116
|
+
} else if (dependencies['{other-router}']) {
|
|
117
|
+
return 'manual';
|
|
118
|
+
}
|
|
119
|
+
return 'default'; // Always have a fallback!
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Features supported by this framework
|
|
124
|
+
*/
|
|
125
|
+
getSupportedFeatures() {
|
|
126
|
+
return {
|
|
127
|
+
oauth: true, // true = full support, 'manual' = guide only, false = none
|
|
128
|
+
stripe: true, // Payment integration
|
|
129
|
+
crm: true, // CRM API access
|
|
130
|
+
projects: true, // Projects feature
|
|
131
|
+
invoices: true, // Invoices feature
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Available generators for this framework
|
|
137
|
+
*/
|
|
138
|
+
getAvailableGenerators() {
|
|
139
|
+
return [
|
|
140
|
+
'api-client', // Always include - generates API client
|
|
141
|
+
'env', // Always include - generates .env.local
|
|
142
|
+
'oauth-guide', // Include if oauth is supported
|
|
143
|
+
// Framework-specific generators:
|
|
144
|
+
// '{framework}-auth',
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = new {Framework}Detector();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Step 2: Register the Detector
|
|
153
|
+
|
|
154
|
+
Add your detector to `src/detectors/registry.js`:
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const nextJsDetector = require('./nextjs-detector');
|
|
158
|
+
const expoDetector = require('./expo-detector');
|
|
159
|
+
const yourDetector = require('./{framework}-detector'); // Add this
|
|
160
|
+
|
|
161
|
+
const detectors = [
|
|
162
|
+
nextJsDetector,
|
|
163
|
+
expoDetector,
|
|
164
|
+
yourDetector, // Add this
|
|
165
|
+
];
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Step 3: Update File Generators (if needed)
|
|
169
|
+
|
|
170
|
+
If your framework needs special file generation, update `src/generators/index.js`:
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
// Check for your framework
|
|
174
|
+
if (frameworkType === '{framework}') {
|
|
175
|
+
// Generate framework-specific files
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
And update `src/generators/api-client-generator.js` for proper file paths:
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
isMobileFramework(frameworkType) {
|
|
183
|
+
return ['expo', 'react-native', '{framework}'].includes(frameworkType);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Step 4: Update Spread Command (if needed)
|
|
188
|
+
|
|
189
|
+
If your framework needs special handling in the setup flow, update `src/commands/spread.js`:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
// Framework-specific detection display
|
|
193
|
+
if (detection.framework.type === '{framework}') {
|
|
194
|
+
console.log(chalk.gray(` Router: ${meta.routerType}`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Framework-specific next steps
|
|
198
|
+
if (frameworkType === '{framework}') {
|
|
199
|
+
console.log(chalk.gray(' 3. Install {framework}-specific package'));
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Step 5: Write Tests
|
|
204
|
+
|
|
205
|
+
Create `tests/{framework}-detector.test.js`:
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
const fs = require('fs');
|
|
209
|
+
const path = require('path');
|
|
210
|
+
|
|
211
|
+
jest.mock('fs');
|
|
212
|
+
|
|
213
|
+
const detector = require('../src/detectors/{framework}-detector');
|
|
214
|
+
|
|
215
|
+
describe('{Framework}Detector', () => {
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
jest.clearAllMocks();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('detects {framework} project', () => {
|
|
221
|
+
fs.existsSync.mockReturnValue(true);
|
|
222
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
223
|
+
dependencies: {
|
|
224
|
+
'{framework-package}': '^1.0.0',
|
|
225
|
+
},
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
const result = detector.detect('/test/project');
|
|
229
|
+
|
|
230
|
+
expect(result.detected).toBe(true);
|
|
231
|
+
expect(result.confidence).toBeGreaterThan(0.8);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('returns correct metadata', () => {
|
|
235
|
+
fs.existsSync.mockImplementation((p) => {
|
|
236
|
+
if (p.includes('tsconfig.json')) return true;
|
|
237
|
+
if (p.includes('package.json')) return true;
|
|
238
|
+
return false;
|
|
239
|
+
});
|
|
240
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
241
|
+
dependencies: {
|
|
242
|
+
'{framework-package}': '^1.0.0',
|
|
243
|
+
'typescript': '^5.0.0',
|
|
244
|
+
},
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
const result = detector.detect('/test/project');
|
|
248
|
+
|
|
249
|
+
expect(result.metadata.hasTypeScript).toBe(true);
|
|
250
|
+
expect(result.metadata.routerType).toBeDefined();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('does not detect non-{framework} project', () => {
|
|
254
|
+
fs.existsSync.mockReturnValue(true);
|
|
255
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
256
|
+
dependencies: {
|
|
257
|
+
'some-other-framework': '^1.0.0',
|
|
258
|
+
},
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
const result = detector.detect('/test/project');
|
|
262
|
+
|
|
263
|
+
expect(result.detected).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Important Rules
|
|
269
|
+
|
|
270
|
+
### 1. Always Return a Router Type
|
|
271
|
+
|
|
272
|
+
The backend expects `routerType` to be a string if present. Never return `null` or `undefined`:
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
// ❌ Bad
|
|
276
|
+
routerType: dependencies['some-router'] ? 'router' : null,
|
|
277
|
+
|
|
278
|
+
// ✅ Good
|
|
279
|
+
routerType: dependencies['some-router'] ? 'specific-router' : 'default',
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 2. Priority Order Matters
|
|
283
|
+
|
|
284
|
+
Higher priority detectors run first. If a project could match multiple detectors (e.g., Next.js is also React), the more specific one should have higher priority.
|
|
285
|
+
|
|
286
|
+
### 3. Confidence Levels
|
|
287
|
+
|
|
288
|
+
- `0.95+`: Very certain (found framework + config file)
|
|
289
|
+
- `0.85-0.94`: Certain (found framework package)
|
|
290
|
+
- `0.70-0.84`: Likely (found related packages)
|
|
291
|
+
- `< 0.70`: Uncertain
|
|
292
|
+
|
|
293
|
+
### 4. Metadata Requirements
|
|
294
|
+
|
|
295
|
+
These fields should always be present in metadata if applicable:
|
|
296
|
+
- `version`: Framework version from package.json
|
|
297
|
+
- `hasTypeScript`: Boolean
|
|
298
|
+
- `routerType`: String (never null!)
|
|
299
|
+
- `config`: Config file name if detected
|
|
300
|
+
|
|
301
|
+
## Example: Adding Remix Support
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
// src/detectors/remix-detector.js
|
|
305
|
+
|
|
306
|
+
const fs = require('fs');
|
|
307
|
+
const path = require('path');
|
|
308
|
+
const BaseDetector = require('./base-detector');
|
|
309
|
+
|
|
310
|
+
class RemixDetector extends BaseDetector {
|
|
311
|
+
get name() {
|
|
312
|
+
return 'remix';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
get priority() {
|
|
316
|
+
return 100; // Meta-framework, high priority
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
detect(projectPath = process.cwd()) {
|
|
320
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
321
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
322
|
+
return { detected: false, confidence: 0, metadata: {} };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
327
|
+
const dependencies = {
|
|
328
|
+
...packageJson.dependencies,
|
|
329
|
+
...packageJson.devDependencies,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Check for Remix
|
|
333
|
+
if (!dependencies['@remix-run/react']) {
|
|
334
|
+
return { detected: false, confidence: 0, metadata: {} };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Detect adapter
|
|
338
|
+
let adapter = 'node'; // default
|
|
339
|
+
if (dependencies['@remix-run/vercel']) adapter = 'vercel';
|
|
340
|
+
else if (dependencies['@remix-run/cloudflare']) adapter = 'cloudflare';
|
|
341
|
+
else if (dependencies['@remix-run/deno']) adapter = 'deno';
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
detected: true,
|
|
345
|
+
confidence: 0.95,
|
|
346
|
+
metadata: {
|
|
347
|
+
version: dependencies['@remix-run/react'],
|
|
348
|
+
hasTypeScript: !!dependencies.typescript ||
|
|
349
|
+
fs.existsSync(path.join(projectPath, 'tsconfig.json')),
|
|
350
|
+
routerType: 'file-based', // Remix always uses file-based routing
|
|
351
|
+
adapter,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
} catch {
|
|
355
|
+
return { detected: false, confidence: 0, metadata: {} };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getSupportedFeatures() {
|
|
360
|
+
return {
|
|
361
|
+
oauth: true,
|
|
362
|
+
stripe: true,
|
|
363
|
+
crm: true,
|
|
364
|
+
projects: true,
|
|
365
|
+
invoices: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
getAvailableGenerators() {
|
|
370
|
+
return ['api-client', 'env', 'oauth-guide'];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
module.exports = new RemixDetector();
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Checklist
|
|
378
|
+
|
|
379
|
+
Before submitting a new detector:
|
|
380
|
+
|
|
381
|
+
- [ ] Detector extends `BaseDetector`
|
|
382
|
+
- [ ] `name` getter returns lowercase string
|
|
383
|
+
- [ ] `priority` is set appropriately (0-100)
|
|
384
|
+
- [ ] `detect()` returns `{ detected, confidence, metadata }`
|
|
385
|
+
- [ ] `routerType` in metadata is never null/undefined
|
|
386
|
+
- [ ] `getSupportedFeatures()` returns feature matrix
|
|
387
|
+
- [ ] `getAvailableGenerators()` returns generator list
|
|
388
|
+
- [ ] Detector is registered in `registry.js`
|
|
389
|
+
- [ ] Tests are written and passing
|
|
390
|
+
- [ ] File generators updated if needed
|
|
391
|
+
- [ ] Spread command updated if needed
|
package/package.json
CHANGED
package/src/commands/spread.js
CHANGED
|
@@ -477,19 +477,26 @@ async function handleSpread() {
|
|
|
477
477
|
}
|
|
478
478
|
} else {
|
|
479
479
|
// Register new application
|
|
480
|
+
// Build source object, only including routerType if it has a value
|
|
481
|
+
const sourceData = {
|
|
482
|
+
type: 'cli',
|
|
483
|
+
projectPathHash,
|
|
484
|
+
cliVersion: pkg.version,
|
|
485
|
+
framework: detection.framework.type || 'unknown',
|
|
486
|
+
frameworkVersion: detection.framework.metadata?.version,
|
|
487
|
+
hasTypeScript: detection.framework.metadata?.hasTypeScript || false,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// Only add routerType if it exists (Next.js has 'app'/'pages', Expo has 'expo-router'/'react-navigation')
|
|
491
|
+
if (detection.framework.metadata?.routerType) {
|
|
492
|
+
sourceData.routerType = detection.framework.metadata.routerType;
|
|
493
|
+
}
|
|
494
|
+
|
|
480
495
|
const registrationData = {
|
|
481
496
|
organizationId,
|
|
482
497
|
name: detection.github.repo || organizationName || 'My Application',
|
|
483
498
|
description: `Connected via CLI from ${detection.framework.type || 'unknown'} project`,
|
|
484
|
-
source:
|
|
485
|
-
type: 'cli',
|
|
486
|
-
projectPathHash,
|
|
487
|
-
cliVersion: pkg.version,
|
|
488
|
-
framework: detection.framework.type || 'unknown',
|
|
489
|
-
frameworkVersion: detection.framework.metadata?.version,
|
|
490
|
-
hasTypeScript: detection.framework.metadata?.hasTypeScript || false,
|
|
491
|
-
routerType: detection.framework.metadata?.routerType,
|
|
492
|
-
},
|
|
499
|
+
source: sourceData,
|
|
493
500
|
connection: {
|
|
494
501
|
features,
|
|
495
502
|
hasFrontendDatabase: !!detection.framework.metadata?.hasPrisma,
|
|
@@ -550,9 +557,17 @@ async function handleSpread() {
|
|
|
550
557
|
console.log(chalk.yellow(' 📋 Next steps:\n'));
|
|
551
558
|
console.log(chalk.gray(' 1. Follow the OAuth setup guide (OAUTH_SETUP_GUIDE.md)'));
|
|
552
559
|
console.log(chalk.gray(' 2. Add OAuth credentials to .env.local'));
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
560
|
+
|
|
561
|
+
// Framework-specific OAuth instructions
|
|
562
|
+
const frameworkType = detection.framework.type;
|
|
563
|
+
if (frameworkType === 'expo' || frameworkType === 'react-native') {
|
|
564
|
+
console.log(chalk.gray(' 3. Install expo-auth-session: npx expo install expo-auth-session expo-crypto'));
|
|
565
|
+
console.log(chalk.gray(' 4. Configure app.json with your OAuth scheme'));
|
|
566
|
+
} else {
|
|
567
|
+
console.log(chalk.gray(' 3. Install NextAuth.js: npm install next-auth'));
|
|
568
|
+
if (oauthProviders.includes('microsoft')) {
|
|
569
|
+
console.log(chalk.gray(' 4. Install Azure AD provider: npm install next-auth/providers/azure-ad'));
|
|
570
|
+
}
|
|
556
571
|
}
|
|
557
572
|
console.log('');
|
|
558
573
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo/React Native Project Detector
|
|
3
|
+
* Detects Expo and React Native projects and their configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const BaseDetector = require('./base-detector');
|
|
9
|
+
|
|
10
|
+
class ExpoDetector extends BaseDetector {
|
|
11
|
+
get name() {
|
|
12
|
+
return 'expo';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get priority() {
|
|
16
|
+
return 95; // High priority - specific framework
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect if current directory is an Expo or React Native project
|
|
21
|
+
*/
|
|
22
|
+
detect(projectPath = process.cwd()) {
|
|
23
|
+
const results = {
|
|
24
|
+
isExpo: false,
|
|
25
|
+
isReactNative: false,
|
|
26
|
+
expoVersion: null,
|
|
27
|
+
reactNativeVersion: null,
|
|
28
|
+
hasTypeScript: false,
|
|
29
|
+
routerType: null, // 'expo-router' or 'react-navigation' or null
|
|
30
|
+
sdkVersion: null,
|
|
31
|
+
config: null,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Check for package.json
|
|
35
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
36
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
37
|
+
return {
|
|
38
|
+
detected: false,
|
|
39
|
+
confidence: 0,
|
|
40
|
+
metadata: {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
46
|
+
const dependencies = {
|
|
47
|
+
...packageJson.dependencies,
|
|
48
|
+
...packageJson.devDependencies,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Check for Expo
|
|
52
|
+
if (dependencies.expo) {
|
|
53
|
+
results.isExpo = true;
|
|
54
|
+
results.expoVersion = dependencies.expo;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for React Native (with or without Expo)
|
|
58
|
+
if (dependencies['react-native']) {
|
|
59
|
+
results.isReactNative = true;
|
|
60
|
+
results.reactNativeVersion = dependencies['react-native'];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If neither, not a React Native project
|
|
64
|
+
if (!results.isExpo && !results.isReactNative) {
|
|
65
|
+
return {
|
|
66
|
+
detected: false,
|
|
67
|
+
confidence: 0,
|
|
68
|
+
metadata: {},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for TypeScript
|
|
73
|
+
if (dependencies.typescript || fs.existsSync(path.join(projectPath, 'tsconfig.json'))) {
|
|
74
|
+
results.hasTypeScript = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Detect router type
|
|
78
|
+
// Default to 'native' (no file-based router) if no router package is found
|
|
79
|
+
if (dependencies['expo-router']) {
|
|
80
|
+
results.routerType = 'expo-router';
|
|
81
|
+
} else if (dependencies['@react-navigation/native']) {
|
|
82
|
+
results.routerType = 'react-navigation';
|
|
83
|
+
} else {
|
|
84
|
+
results.routerType = 'native'; // Basic React Native navigation
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for app.json or app.config.js (Expo config)
|
|
88
|
+
const appJsonPath = path.join(projectPath, 'app.json');
|
|
89
|
+
const appConfigJsPath = path.join(projectPath, 'app.config.js');
|
|
90
|
+
const appConfigTsPath = path.join(projectPath, 'app.config.ts');
|
|
91
|
+
|
|
92
|
+
if (fs.existsSync(appJsonPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf8'));
|
|
95
|
+
results.config = 'app.json';
|
|
96
|
+
if (appJson.expo?.sdkVersion) {
|
|
97
|
+
results.sdkVersion = appJson.expo.sdkVersion;
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Could not parse app.json
|
|
101
|
+
}
|
|
102
|
+
} else if (fs.existsSync(appConfigJsPath)) {
|
|
103
|
+
results.config = 'app.config.js';
|
|
104
|
+
} else if (fs.existsSync(appConfigTsPath)) {
|
|
105
|
+
results.config = 'app.config.ts';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Determine confidence based on what we found
|
|
109
|
+
let confidence = 0.7; // Base confidence for React Native
|
|
110
|
+
if (results.isExpo) {
|
|
111
|
+
confidence = 0.9; // Higher for Expo
|
|
112
|
+
if (results.config) {
|
|
113
|
+
confidence = 0.95; // Even higher with config file
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
detected: true,
|
|
119
|
+
confidence,
|
|
120
|
+
metadata: {
|
|
121
|
+
isExpo: results.isExpo,
|
|
122
|
+
expoVersion: results.expoVersion,
|
|
123
|
+
reactNativeVersion: results.reactNativeVersion,
|
|
124
|
+
hasTypeScript: results.hasTypeScript,
|
|
125
|
+
routerType: results.routerType,
|
|
126
|
+
sdkVersion: results.sdkVersion,
|
|
127
|
+
config: results.config,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Error reading package.json
|
|
132
|
+
return {
|
|
133
|
+
detected: false,
|
|
134
|
+
confidence: 0,
|
|
135
|
+
metadata: {},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get supported features for Expo/React Native
|
|
142
|
+
*/
|
|
143
|
+
getSupportedFeatures() {
|
|
144
|
+
return {
|
|
145
|
+
oauth: true, // OAuth via expo-auth-session or similar
|
|
146
|
+
stripe: true, // Stripe via @stripe/stripe-react-native
|
|
147
|
+
crm: true, // CRM API access
|
|
148
|
+
projects: true, // Projects API access
|
|
149
|
+
invoices: true, // Invoices API access
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get available generators for Expo/React Native
|
|
155
|
+
*/
|
|
156
|
+
getAvailableGenerators() {
|
|
157
|
+
return [
|
|
158
|
+
'api-client', // TypeScript API client
|
|
159
|
+
'env', // Environment variables
|
|
160
|
+
'oauth-guide', // OAuth setup guide for mobile
|
|
161
|
+
// Future: 'expo-auth', 'stripe-mobile'
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = new ExpoDetector();
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const nextJsDetector = require('./nextjs-detector');
|
|
9
|
+
const expoDetector = require('./expo-detector');
|
|
9
10
|
// Future detectors will be added here:
|
|
10
11
|
// const reactDetector = require('./react-detector');
|
|
11
12
|
// const vueDetector = require('./vue-detector');
|
|
@@ -16,6 +17,7 @@ const nextJsDetector = require('./nextjs-detector');
|
|
|
16
17
|
*/
|
|
17
18
|
const detectors = [
|
|
18
19
|
nextJsDetector,
|
|
20
|
+
expoDetector,
|
|
19
21
|
// Future: reactDetector, vueDetector, etc.
|
|
20
22
|
];
|
|
21
23
|
|
|
@@ -8,6 +8,13 @@ const path = require('path');
|
|
|
8
8
|
const { checkFileOverwrite, writeFileWithBackup, ensureDir } = require('../utils/file-utils');
|
|
9
9
|
|
|
10
10
|
class ApiClientGenerator {
|
|
11
|
+
/**
|
|
12
|
+
* Check if framework is a mobile platform
|
|
13
|
+
*/
|
|
14
|
+
isMobileFramework(frameworkType) {
|
|
15
|
+
return ['expo', 'react-native'].includes(frameworkType);
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Generate API client file
|
|
13
20
|
* @param {Object} options - Generation options
|
|
@@ -20,18 +27,26 @@ class ApiClientGenerator {
|
|
|
20
27
|
backendUrl,
|
|
21
28
|
organizationId,
|
|
22
29
|
isTypeScript,
|
|
30
|
+
frameworkType,
|
|
23
31
|
} = options;
|
|
24
32
|
|
|
25
|
-
// Determine output path based on project structure
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
:
|
|
33
|
+
// Determine output path based on framework and project structure
|
|
34
|
+
let outputDir;
|
|
35
|
+
if (this.isMobileFramework(frameworkType)) {
|
|
36
|
+
// Expo/React Native: use src/api or src/services
|
|
37
|
+
outputDir = path.join(projectPath, 'src', 'api');
|
|
38
|
+
} else {
|
|
39
|
+
// Next.js/Web: use lib or src/lib
|
|
40
|
+
outputDir = fs.existsSync(path.join(projectPath, 'src'))
|
|
41
|
+
? path.join(projectPath, 'src', 'lib')
|
|
42
|
+
: path.join(projectPath, 'lib');
|
|
43
|
+
}
|
|
29
44
|
|
|
30
|
-
// Ensure
|
|
31
|
-
ensureDir(
|
|
45
|
+
// Ensure output directory exists
|
|
46
|
+
ensureDir(outputDir);
|
|
32
47
|
|
|
33
48
|
const extension = isTypeScript ? 'ts' : 'js';
|
|
34
|
-
const outputPath = path.join(
|
|
49
|
+
const outputPath = path.join(outputDir, `api-client.${extension}`);
|
|
35
50
|
|
|
36
51
|
// Check if file exists and get action
|
|
37
52
|
const action = await checkFileOverwrite(outputPath);
|
package/src/generators/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File Generators
|
|
3
3
|
* Main entry point for all file generation
|
|
4
|
+
*
|
|
5
|
+
* Supports multiple frameworks:
|
|
6
|
+
* - nextjs: Next.js (App Router and Pages Router)
|
|
7
|
+
* - expo: Expo/React Native
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
const apiClientGenerator = require('./api-client-generator');
|
|
@@ -10,6 +14,20 @@ const oauthGuideGenerator = require('./oauth-guide-generator');
|
|
|
10
14
|
const gitignoreGenerator = require('./gitignore-generator');
|
|
11
15
|
|
|
12
16
|
class FileGenerator {
|
|
17
|
+
/**
|
|
18
|
+
* Check if framework is a mobile platform
|
|
19
|
+
*/
|
|
20
|
+
isMobileFramework(frameworkType) {
|
|
21
|
+
return ['expo', 'react-native'].includes(frameworkType);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if framework is Next.js
|
|
26
|
+
*/
|
|
27
|
+
isNextJs(frameworkType) {
|
|
28
|
+
return frameworkType === 'nextjs';
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Generate all files based on configuration
|
|
15
33
|
*/
|
|
@@ -22,6 +40,10 @@ class FileGenerator {
|
|
|
22
40
|
gitignore: null,
|
|
23
41
|
};
|
|
24
42
|
|
|
43
|
+
const frameworkType = options.frameworkType || 'unknown';
|
|
44
|
+
const isMobile = this.isMobileFramework(frameworkType);
|
|
45
|
+
const isNextJs = this.isNextJs(frameworkType);
|
|
46
|
+
|
|
25
47
|
// Generate API client (async - checks for file overwrites)
|
|
26
48
|
if (options.features && options.features.length > 0) {
|
|
27
49
|
results.apiClient = await apiClientGenerator.generate(options);
|
|
@@ -30,14 +52,22 @@ class FileGenerator {
|
|
|
30
52
|
// Generate environment file
|
|
31
53
|
results.envFile = envGenerator.generate(options);
|
|
32
54
|
|
|
33
|
-
// Generate NextAuth.js config if OAuth is enabled (
|
|
55
|
+
// Generate NextAuth.js config if OAuth is enabled (Next.js only)
|
|
34
56
|
if (options.features && options.features.includes('oauth') && options.oauthProviders) {
|
|
35
|
-
|
|
57
|
+
if (isNextJs) {
|
|
58
|
+
results.nextauth = await nextauthGenerator.generate(options);
|
|
59
|
+
}
|
|
60
|
+
// For mobile, OAuth is handled differently (expo-auth-session, etc.)
|
|
36
61
|
}
|
|
37
62
|
|
|
38
63
|
// Generate OAuth guide if OAuth is enabled
|
|
39
64
|
if (options.features && options.features.includes('oauth') && options.oauthProviders) {
|
|
40
|
-
|
|
65
|
+
// Pass framework info so guide can be customized
|
|
66
|
+
results.oauthGuide = oauthGuideGenerator.generate({
|
|
67
|
+
...options,
|
|
68
|
+
isMobile,
|
|
69
|
+
isNextJs,
|
|
70
|
+
});
|
|
41
71
|
}
|
|
42
72
|
|
|
43
73
|
// Update .gitignore
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Expo/React Native Project Detector
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
jest.mock('fs');
|
|
9
|
+
|
|
10
|
+
const detector = require('../src/detectors/expo-detector');
|
|
11
|
+
|
|
12
|
+
describe('ExpoDetector', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('basic detection', () => {
|
|
18
|
+
it('detects Expo project', () => {
|
|
19
|
+
fs.existsSync.mockReturnValue(true);
|
|
20
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
21
|
+
dependencies: {
|
|
22
|
+
expo: '^51.0.0',
|
|
23
|
+
'react-native': '0.74.0',
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const result = detector.detect('/test/project');
|
|
28
|
+
|
|
29
|
+
expect(result.detected).toBe(true);
|
|
30
|
+
expect(result.confidence).toBeGreaterThan(0.8);
|
|
31
|
+
expect(result.metadata.isExpo).toBe(true);
|
|
32
|
+
expect(result.metadata.expoVersion).toBe('^51.0.0');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('detects React Native project without Expo (below confidence threshold)', () => {
|
|
36
|
+
fs.existsSync.mockReturnValue(true);
|
|
37
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
38
|
+
dependencies: {
|
|
39
|
+
'react-native': '0.74.0',
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const result = detector.detect('/test/project');
|
|
44
|
+
|
|
45
|
+
// Pure React Native has lower confidence (0.7)
|
|
46
|
+
// which is below the 0.8 threshold, so it won't be the "detected" match
|
|
47
|
+
// but the detector still returns useful data
|
|
48
|
+
expect(result.detected).toBe(true);
|
|
49
|
+
expect(result.confidence).toBe(0.7);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not detect non-Expo/React Native project', () => {
|
|
53
|
+
fs.existsSync.mockReturnValue(true);
|
|
54
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
55
|
+
dependencies: {
|
|
56
|
+
next: '^14.0.0',
|
|
57
|
+
react: '^18.0.0',
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const result = detector.detect('/test/project');
|
|
62
|
+
|
|
63
|
+
expect(result.detected).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not detect when no package.json', () => {
|
|
67
|
+
fs.existsSync.mockReturnValue(false);
|
|
68
|
+
|
|
69
|
+
const result = detector.detect('/test/project');
|
|
70
|
+
|
|
71
|
+
expect(result.detected).toBe(false);
|
|
72
|
+
expect(result.confidence).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('routerType detection', () => {
|
|
77
|
+
it('detects expo-router', () => {
|
|
78
|
+
fs.existsSync.mockReturnValue(true);
|
|
79
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
80
|
+
dependencies: {
|
|
81
|
+
expo: '^51.0.0',
|
|
82
|
+
'expo-router': '^3.5.0',
|
|
83
|
+
'react-native': '0.74.0',
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const result = detector.detect('/test/project');
|
|
88
|
+
|
|
89
|
+
expect(result.detected).toBe(true);
|
|
90
|
+
expect(result.metadata.routerType).toBe('expo-router');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('detects react-navigation', () => {
|
|
94
|
+
fs.existsSync.mockReturnValue(true);
|
|
95
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
96
|
+
dependencies: {
|
|
97
|
+
expo: '^51.0.0',
|
|
98
|
+
'@react-navigation/native': '^6.0.0',
|
|
99
|
+
'react-native': '0.74.0',
|
|
100
|
+
},
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
const result = detector.detect('/test/project');
|
|
104
|
+
|
|
105
|
+
expect(result.detected).toBe(true);
|
|
106
|
+
expect(result.metadata.routerType).toBe('react-navigation');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('defaults to native when no router package is present', () => {
|
|
110
|
+
fs.existsSync.mockReturnValue(true);
|
|
111
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
112
|
+
dependencies: {
|
|
113
|
+
expo: '^51.0.0',
|
|
114
|
+
'react-native': '0.74.0',
|
|
115
|
+
},
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
const result = detector.detect('/test/project');
|
|
119
|
+
|
|
120
|
+
expect(result.detected).toBe(true);
|
|
121
|
+
expect(result.metadata.routerType).toBe('native');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('NEVER returns null for routerType when project is detected', () => {
|
|
125
|
+
// This is the critical test - routerType must never be null
|
|
126
|
+
// when the project is successfully detected
|
|
127
|
+
fs.existsSync.mockReturnValue(true);
|
|
128
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
129
|
+
dependencies: {
|
|
130
|
+
expo: '^51.0.0',
|
|
131
|
+
'react-native': '0.74.0',
|
|
132
|
+
// No router packages at all
|
|
133
|
+
},
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const result = detector.detect('/test/project');
|
|
137
|
+
|
|
138
|
+
expect(result.detected).toBe(true);
|
|
139
|
+
// This must NEVER be null - backend requires string
|
|
140
|
+
expect(result.metadata.routerType).not.toBeNull();
|
|
141
|
+
expect(result.metadata.routerType).not.toBeUndefined();
|
|
142
|
+
expect(typeof result.metadata.routerType).toBe('string');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('TypeScript detection', () => {
|
|
147
|
+
it('detects TypeScript via dependency', () => {
|
|
148
|
+
fs.existsSync.mockReturnValue(true);
|
|
149
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
150
|
+
dependencies: {
|
|
151
|
+
expo: '^51.0.0',
|
|
152
|
+
'react-native': '0.74.0',
|
|
153
|
+
},
|
|
154
|
+
devDependencies: {
|
|
155
|
+
typescript: '^5.0.0',
|
|
156
|
+
},
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
const result = detector.detect('/test/project');
|
|
160
|
+
|
|
161
|
+
expect(result.metadata.hasTypeScript).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('detects TypeScript via tsconfig.json', () => {
|
|
165
|
+
fs.existsSync.mockImplementation((p) => {
|
|
166
|
+
if (p.includes('tsconfig.json')) return true;
|
|
167
|
+
if (p.includes('package.json')) return true;
|
|
168
|
+
return false;
|
|
169
|
+
});
|
|
170
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
171
|
+
dependencies: {
|
|
172
|
+
expo: '^51.0.0',
|
|
173
|
+
'react-native': '0.74.0',
|
|
174
|
+
},
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
const result = detector.detect('/test/project');
|
|
178
|
+
|
|
179
|
+
expect(result.metadata.hasTypeScript).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('app config detection', () => {
|
|
184
|
+
it('detects app.json config', () => {
|
|
185
|
+
fs.existsSync.mockImplementation((p) => {
|
|
186
|
+
if (p.includes('app.json')) return true;
|
|
187
|
+
if (p.includes('package.json')) return true;
|
|
188
|
+
return false;
|
|
189
|
+
});
|
|
190
|
+
fs.readFileSync.mockImplementation((p) => {
|
|
191
|
+
if (p.includes('app.json')) {
|
|
192
|
+
return JSON.stringify({
|
|
193
|
+
expo: {
|
|
194
|
+
sdkVersion: '51.0.0',
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return JSON.stringify({
|
|
199
|
+
dependencies: {
|
|
200
|
+
expo: '^51.0.0',
|
|
201
|
+
'react-native': '0.74.0',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const result = detector.detect('/test/project');
|
|
207
|
+
|
|
208
|
+
expect(result.metadata.config).toBe('app.json');
|
|
209
|
+
expect(result.metadata.sdkVersion).toBe('51.0.0');
|
|
210
|
+
expect(result.confidence).toBe(0.95);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('detects app.config.js config', () => {
|
|
214
|
+
fs.existsSync.mockImplementation((p) => {
|
|
215
|
+
if (p.includes('app.config.js')) return true;
|
|
216
|
+
if (p.includes('package.json')) return true;
|
|
217
|
+
return false;
|
|
218
|
+
});
|
|
219
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({
|
|
220
|
+
dependencies: {
|
|
221
|
+
expo: '^51.0.0',
|
|
222
|
+
'react-native': '0.74.0',
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
const result = detector.detect('/test/project');
|
|
227
|
+
|
|
228
|
+
expect(result.metadata.config).toBe('app.config.js');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('features and generators', () => {
|
|
233
|
+
it('returns supported features', () => {
|
|
234
|
+
const features = detector.getSupportedFeatures();
|
|
235
|
+
|
|
236
|
+
expect(features.oauth).toBe(true);
|
|
237
|
+
expect(features.stripe).toBe(true);
|
|
238
|
+
expect(features.crm).toBe(true);
|
|
239
|
+
expect(features.projects).toBe(true);
|
|
240
|
+
expect(features.invoices).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns available generators', () => {
|
|
244
|
+
const generators = detector.getAvailableGenerators();
|
|
245
|
+
|
|
246
|
+
expect(generators).toContain('api-client');
|
|
247
|
+
expect(generators).toContain('env');
|
|
248
|
+
expect(generators).toContain('oauth-guide');
|
|
249
|
+
// Should NOT include Next.js specific generators
|
|
250
|
+
expect(generators).not.toContain('nextauth');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('detector properties', () => {
|
|
255
|
+
it('has correct name', () => {
|
|
256
|
+
expect(detector.name).toBe('expo');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('has high priority', () => {
|
|
260
|
+
expect(detector.priority).toBe(95);
|
|
261
|
+
// Should be higher than generic React but lower than Expo with config
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -98,7 +98,7 @@ describe('FileGenerator', () => {
|
|
|
98
98
|
expect(result.envFile).toContain('.env.local');
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
it('generates NextAuth config when oauth feature enabled', async () => {
|
|
101
|
+
it('generates NextAuth config when oauth feature enabled (Next.js)', async () => {
|
|
102
102
|
const options = {
|
|
103
103
|
projectPath: mockProjectPath,
|
|
104
104
|
apiKey: 'test-key',
|
|
@@ -108,6 +108,7 @@ describe('FileGenerator', () => {
|
|
|
108
108
|
oauthProviders: ['google'],
|
|
109
109
|
isTypeScript: false,
|
|
110
110
|
routerType: 'app',
|
|
111
|
+
frameworkType: 'nextjs', // NextAuth is Next.js only
|
|
111
112
|
};
|
|
112
113
|
|
|
113
114
|
const result = await FileGenerator.generate(options);
|
|
@@ -115,6 +116,23 @@ describe('FileGenerator', () => {
|
|
|
115
116
|
expect(result.nextauth).not.toBeNull();
|
|
116
117
|
});
|
|
117
118
|
|
|
119
|
+
it('does not generate NextAuth for Expo/mobile apps', async () => {
|
|
120
|
+
const options = {
|
|
121
|
+
projectPath: mockProjectPath,
|
|
122
|
+
apiKey: 'test-key',
|
|
123
|
+
backendUrl: 'https://backend.test.com',
|
|
124
|
+
organizationId: 'org-123',
|
|
125
|
+
features: ['oauth'],
|
|
126
|
+
oauthProviders: ['google'],
|
|
127
|
+
isTypeScript: true,
|
|
128
|
+
frameworkType: 'expo',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = await FileGenerator.generate(options);
|
|
132
|
+
|
|
133
|
+
expect(result.nextauth).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
118
136
|
it('does not generate NextAuth when oauth not in features', async () => {
|
|
119
137
|
const options = {
|
|
120
138
|
projectPath: mockProjectPath,
|
|
@@ -198,7 +216,7 @@ describe('FileGenerator', () => {
|
|
|
198
216
|
expect(result).toHaveProperty('gitignore');
|
|
199
217
|
});
|
|
200
218
|
|
|
201
|
-
it('generates all files when all features enabled', async () => {
|
|
219
|
+
it('generates all files when all features enabled (Next.js)', async () => {
|
|
202
220
|
const options = {
|
|
203
221
|
projectPath: mockProjectPath,
|
|
204
222
|
apiKey: 'test-key',
|
|
@@ -210,6 +228,7 @@ describe('FileGenerator', () => {
|
|
|
210
228
|
routerType: 'app',
|
|
211
229
|
productionDomain: 'example.com',
|
|
212
230
|
appName: 'Full App',
|
|
231
|
+
frameworkType: 'nextjs',
|
|
213
232
|
};
|
|
214
233
|
|
|
215
234
|
const result = await FileGenerator.generate(options);
|