@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l4yercak3/cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Icing on the L4yercak3 - The sweet finishing touch for your Layer Cake integration",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- console.log(chalk.gray(' 3. Install NextAuth.js: npm install next-auth'));
554
- if (oauthProviders.includes('microsoft')) {
555
- console.log(chalk.gray(' 4. Install Azure AD provider: npm install next-auth/providers/azure-ad'));
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
- const libDir = fs.existsSync(path.join(projectPath, 'src'))
27
- ? path.join(projectPath, 'src', 'lib')
28
- : path.join(projectPath, 'lib');
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 lib directory exists
31
- ensureDir(libDir);
45
+ // Ensure output directory exists
46
+ ensureDir(outputDir);
32
47
 
33
48
  const extension = isTypeScript ? 'ts' : 'js';
34
- const outputPath = path.join(libDir, `api-client.${extension}`);
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);
@@ -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 (async - checks for file overwrites)
55
+ // Generate NextAuth.js config if OAuth is enabled (Next.js only)
34
56
  if (options.features && options.features.includes('oauth') && options.oauthProviders) {
35
- results.nextauth = await nextauthGenerator.generate(options);
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
- results.oauthGuide = oauthGuideGenerator.generate(options);
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);