@l4yercak3/cli 1.2.1 → 1.2.3
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 +31 -12
- package/src/detectors/expo-detector.js +3 -0
- package/src/generators/api-client-generator.js +28 -9
- package/src/generators/env-generator.js +2 -1
- 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
|
@@ -242,6 +242,7 @@ async function handleSpread() {
|
|
|
242
242
|
value: key.id,
|
|
243
243
|
}));
|
|
244
244
|
keyChoices.push({ name: '➕ Generate a new API key', value: '__generate__' });
|
|
245
|
+
keyChoices.push({ name: '⏭️ Skip - I already have my key configured', value: '__skip__' });
|
|
245
246
|
|
|
246
247
|
const { keyChoice } = await inquirer.prompt([
|
|
247
248
|
{
|
|
@@ -254,6 +255,9 @@ async function handleSpread() {
|
|
|
254
255
|
|
|
255
256
|
if (keyChoice === '__generate__') {
|
|
256
257
|
apiKey = await generateNewApiKey(organizationId);
|
|
258
|
+
} else if (keyChoice === '__skip__') {
|
|
259
|
+
apiKey = null; // Will skip writing API key to .env.local
|
|
260
|
+
console.log(chalk.green(` ✅ Skipping API key setup - using existing configuration\n`));
|
|
257
261
|
} else {
|
|
258
262
|
// User selected existing key - we only have the preview, not the full key
|
|
259
263
|
// They need to use the key they have stored or get it from dashboard
|
|
@@ -477,19 +481,26 @@ async function handleSpread() {
|
|
|
477
481
|
}
|
|
478
482
|
} else {
|
|
479
483
|
// Register new application
|
|
484
|
+
// Build source object, only including routerType if it has a value
|
|
485
|
+
const sourceData = {
|
|
486
|
+
type: 'cli',
|
|
487
|
+
projectPathHash,
|
|
488
|
+
cliVersion: pkg.version,
|
|
489
|
+
framework: detection.framework.type || 'unknown',
|
|
490
|
+
frameworkVersion: detection.framework.metadata?.version,
|
|
491
|
+
hasTypeScript: detection.framework.metadata?.hasTypeScript || false,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Only add routerType if it exists (Next.js has 'app'/'pages', Expo has 'expo-router'/'react-navigation')
|
|
495
|
+
if (detection.framework.metadata?.routerType) {
|
|
496
|
+
sourceData.routerType = detection.framework.metadata.routerType;
|
|
497
|
+
}
|
|
498
|
+
|
|
480
499
|
const registrationData = {
|
|
481
500
|
organizationId,
|
|
482
501
|
name: detection.github.repo || organizationName || 'My Application',
|
|
483
502
|
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
|
-
},
|
|
503
|
+
source: sourceData,
|
|
493
504
|
connection: {
|
|
494
505
|
features,
|
|
495
506
|
hasFrontendDatabase: !!detection.framework.metadata?.hasPrisma,
|
|
@@ -550,9 +561,17 @@ async function handleSpread() {
|
|
|
550
561
|
console.log(chalk.yellow(' 📋 Next steps:\n'));
|
|
551
562
|
console.log(chalk.gray(' 1. Follow the OAuth setup guide (OAUTH_SETUP_GUIDE.md)'));
|
|
552
563
|
console.log(chalk.gray(' 2. Add OAuth credentials to .env.local'));
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
564
|
+
|
|
565
|
+
// Framework-specific OAuth instructions
|
|
566
|
+
const frameworkType = detection.framework.type;
|
|
567
|
+
if (frameworkType === 'expo' || frameworkType === 'react-native') {
|
|
568
|
+
console.log(chalk.gray(' 3. Install expo-auth-session: npx expo install expo-auth-session expo-crypto'));
|
|
569
|
+
console.log(chalk.gray(' 4. Configure app.json with your OAuth scheme'));
|
|
570
|
+
} else {
|
|
571
|
+
console.log(chalk.gray(' 3. Install NextAuth.js: npm install next-auth'));
|
|
572
|
+
if (oauthProviders.includes('microsoft')) {
|
|
573
|
+
console.log(chalk.gray(' 4. Install Azure AD provider: npm install next-auth/providers/azure-ad'));
|
|
574
|
+
}
|
|
556
575
|
}
|
|
557
576
|
console.log('');
|
|
558
577
|
}
|
|
@@ -75,10 +75,13 @@ class ExpoDetector extends BaseDetector {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Detect router type
|
|
78
|
+
// Default to 'native' (no file-based router) if no router package is found
|
|
78
79
|
if (dependencies['expo-router']) {
|
|
79
80
|
results.routerType = 'expo-router';
|
|
80
81
|
} else if (dependencies['@react-navigation/native']) {
|
|
81
82
|
results.routerType = 'react-navigation';
|
|
83
|
+
} else {
|
|
84
|
+
results.routerType = 'native'; // Basic React Native navigation
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
// Check for app.json or app.config.js (Expo config)
|
|
@@ -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);
|
|
@@ -56,11 +71,15 @@ class ApiClientGenerator {
|
|
|
56
71
|
*/
|
|
57
72
|
generateCode({ apiKey, backendUrl, organizationId, isTypeScript }) {
|
|
58
73
|
const returnType = isTypeScript ? ': Promise<any>' : '';
|
|
74
|
+
// If apiKey is null (user skipped), use environment variable reference
|
|
75
|
+
const apiKeyDefault = apiKey
|
|
76
|
+
? `'${apiKey}'`
|
|
77
|
+
: `process.env.L4YERCAK3_API_KEY || ''`;
|
|
59
78
|
|
|
60
79
|
return `/**
|
|
61
80
|
* L4YERCAK3 API Client
|
|
62
81
|
* Auto-generated by @l4yercak3/cli
|
|
63
|
-
*
|
|
82
|
+
*
|
|
64
83
|
* This client handles authenticated requests to the L4YERCAK3 backend API.
|
|
65
84
|
* Organization ID: ${organizationId}
|
|
66
85
|
*/
|
|
@@ -68,7 +87,7 @@ class ApiClientGenerator {
|
|
|
68
87
|
// Using native fetch (available in Next.js 13+)
|
|
69
88
|
|
|
70
89
|
class L4YERCAK3Client {
|
|
71
|
-
constructor(apiKey${isTypeScript ? ': string' : ''} =
|
|
90
|
+
constructor(apiKey${isTypeScript ? ': string' : ''} = ${apiKeyDefault}, baseUrl${isTypeScript ? ': string' : ''} = '${backendUrl}') {
|
|
72
91
|
this.apiKey = apiKey;
|
|
73
92
|
this.baseUrl = baseUrl;
|
|
74
93
|
this.organizationId = '${organizationId}';
|
|
@@ -27,7 +27,8 @@ class EnvGenerator {
|
|
|
27
27
|
const envVars = {
|
|
28
28
|
...existingEnv,
|
|
29
29
|
// Core API configuration
|
|
30
|
-
|
|
30
|
+
// If apiKey is null (user skipped), preserve existing value
|
|
31
|
+
L4YERCAK3_API_KEY: apiKey || existingEnv.L4YERCAK3_API_KEY || 'your_api_key_here',
|
|
31
32
|
L4YERCAK3_BACKEND_URL: backendUrl,
|
|
32
33
|
L4YERCAK3_ORGANIZATION_ID: organizationId,
|
|
33
34
|
NEXT_PUBLIC_L4YERCAK3_BACKEND_URL: backendUrl,
|
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);
|