create-switch-framework-app 0.1.0 → 0.2.0
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/README.md +25 -25
- package/bin/create-switch-framework-app.js +510 -502
- package/package.json +31 -31
- package/templates/electron/base/app/(tabs)/+not-found.js +157 -157
- package/templates/electron/base/app/(tabs)/_layout.js +57 -93
- package/templates/electron/base/app/(tabs)/explore.js +55 -44
- package/templates/electron/base/app/(tabs)/index.js +10 -24
- package/templates/electron/base/app/+not-found.js +148 -158
- package/templates/electron/base/app/_layout.js +24 -44
- package/templates/electron/base/app/index.js +16 -30
- package/templates/electron/base/assets/logo.svg +5 -5
- package/templates/electron/base/components/SwSplashScreen.js +1 -1
- package/templates/electron/base/components/SwStarterSplashScreen.js +130 -140
- package/templates/electron/base/components/SwTabBar.js +146 -153
- package/templates/electron/base/electron/electron-builder.json +19 -19
- package/templates/electron/base/electron/main.js +30 -30
- package/templates/electron/base/electron/preload.js +5 -5
- package/templates/electron/base/index.js +2 -3
- package/templates/electron/base/main.js +1 -1
- package/templates/electron/base/preload.js +1 -1
- package/templates/electron/base/server.js +27 -42
- package/templates/web/base/app/(tabs)/+not-found.js +157 -157
- package/templates/web/base/app/(tabs)/_layout.js +57 -93
- package/templates/web/base/app/(tabs)/explore.js +55 -44
- package/templates/web/base/app/(tabs)/index.js +10 -24
- package/templates/web/base/app/+not-found.js +148 -158
- package/templates/web/base/app/_layout.js +24 -44
- package/templates/web/base/app/index.js +16 -30
- package/templates/web/base/assets/logo.svg +5 -5
- package/templates/web/base/components/SwSplashScreen.js +1 -1
- package/templates/web/base/components/SwStarterSplashScreen.js +130 -140
- package/templates/web/base/components/SwTabBar.js +146 -153
- package/templates/web/base/index.js +2 -3
- package/templates/electron/base/app/(tabs)/register.js +0 -12
- package/templates/electron/base/app/register.js +0 -12
- package/templates/web/base/app/(tabs)/register.js +0 -12
- package/templates/web/base/app/register.js +0 -12
|
@@ -1,502 +1,510 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { createRequire } from 'node:module';
|
|
7
|
-
|
|
8
|
-
import fs from 'fs-extra';
|
|
9
|
-
import chalk from 'chalk';
|
|
10
|
-
import ora from 'ora';
|
|
11
|
-
import enquirer from 'enquirer';
|
|
12
|
-
|
|
13
|
-
const { prompt } = enquirer;
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
|
|
16
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
-
const __dirname = path.dirname(__filename);
|
|
18
|
-
|
|
19
|
-
function printHelp() {
|
|
20
|
-
console.log(`\n${chalk.bold('create-switch-framework-app')}\n`);
|
|
21
|
-
console.log('Usage:');
|
|
22
|
-
console.log(' npx create-switch-framework-app <project-name> [options]');
|
|
23
|
-
console.log('\nOptions:');
|
|
24
|
-
console.log(' --yes, -y Skip prompts and use defaults');
|
|
25
|
-
console.log(' --app-type One of: web | electron | both');
|
|
26
|
-
console.log(' --port Server port (1-65535)');
|
|
27
|
-
console.log(' --no-install Do not run npm install');
|
|
28
|
-
console.log(' --use-local Use npm link for switch-framework + switch-framework-backend (no npm registry)');
|
|
29
|
-
console.log(' -h, --help Show help');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseArgs(argv) {
|
|
33
|
-
const args = argv.slice(2);
|
|
34
|
-
const flags = new Set(args.filter(a => a.startsWith('-')));
|
|
35
|
-
|
|
36
|
-
const help = flags.has('-h') || flags.has('--help');
|
|
37
|
-
const yes = flags.has('--yes') || flags.has('-y');
|
|
38
|
-
const noInstall = flags.has('--no-install');
|
|
39
|
-
const useLocal = flags.has('--use-local');
|
|
40
|
-
|
|
41
|
-
const getValue = (name) => {
|
|
42
|
-
const i = args.indexOf(name);
|
|
43
|
-
if (i === -1) return null;
|
|
44
|
-
const v = args[i + 1];
|
|
45
|
-
if (!v || v.startsWith('-')) return null;
|
|
46
|
-
return v;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const appType = getValue('--app-type');
|
|
50
|
-
const port = getValue('--port');
|
|
51
|
-
|
|
52
|
-
const positional = args.filter(a => !a.startsWith('-'));
|
|
53
|
-
const projectName = positional[0] || null;
|
|
54
|
-
|
|
55
|
-
return { help, yes, noInstall, useLocal, projectName, appType, port };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function sanitizeProjectName(input) {
|
|
59
|
-
const name = String(input || '').trim();
|
|
60
|
-
return name.replace(/[\\/]/g, '-');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function toPackageName(projectName) {
|
|
64
|
-
return projectName
|
|
65
|
-
.toLowerCase()
|
|
66
|
-
.replace(/\s+/g, '-')
|
|
67
|
-
.replace(/[^a-z0-9-_]/g, '-')
|
|
68
|
-
.replace(/-+/g, '-')
|
|
69
|
-
.replace(/^-+|-+$/g, '') || 'switch-framework-app';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function askQuestions({ projectName, yes, noInstall, appTypeOverride, portOverride }) {
|
|
73
|
-
const defaults = {
|
|
74
|
-
appType: 'web',
|
|
75
|
-
port: 3000,
|
|
76
|
-
install: true
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const resolvedAppType = appTypeOverride || defaults.appType;
|
|
80
|
-
const resolvedPort = portOverride != null ? Number(portOverride) : defaults.port;
|
|
81
|
-
|
|
82
|
-
if (yes) {
|
|
83
|
-
return {
|
|
84
|
-
projectName: projectName || 'switch-framework-app',
|
|
85
|
-
appType: resolvedAppType,
|
|
86
|
-
port: resolvedPort,
|
|
87
|
-
install: noInstall ? false : defaults.install
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const questions = [];
|
|
92
|
-
|
|
93
|
-
if (!projectName) {
|
|
94
|
-
questions.push({
|
|
95
|
-
type: 'input',
|
|
96
|
-
name: 'projectName',
|
|
97
|
-
message: 'Project name',
|
|
98
|
-
initial: 'switch-framework-app',
|
|
99
|
-
validate(value) {
|
|
100
|
-
const v = sanitizeProjectName(value);
|
|
101
|
-
if (!v) return 'Project name is required';
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
questions.push(
|
|
108
|
-
{
|
|
109
|
-
type: 'select',
|
|
110
|
-
name: 'appType',
|
|
111
|
-
message: 'What type of app do you want to create?',
|
|
112
|
-
choices: [
|
|
113
|
-
{ name: 'web', message: 'Web App (browser + Node.js/Express backend)' },
|
|
114
|
-
{ name: 'electron', message: 'Electron Desktop App (with shared Express backend)' },
|
|
115
|
-
{ name: 'both', message: 'Both (monorepo with web + electron targets)' }
|
|
116
|
-
],
|
|
117
|
-
initial: resolvedAppType
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
type: 'numeral',
|
|
121
|
-
name: 'port',
|
|
122
|
-
message: 'Which port do you want the server to use?',
|
|
123
|
-
initial: resolvedPort,
|
|
124
|
-
validate(value) {
|
|
125
|
-
const n = Number(value);
|
|
126
|
-
if (!Number.isInteger(n) || n < 1 || n > 65535) return 'Enter a valid port (1-65535)';
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
type: 'confirm',
|
|
132
|
-
name: 'install',
|
|
133
|
-
message: 'Do you want to install dependencies automatically?',
|
|
134
|
-
initial: defaults.install,
|
|
135
|
-
skip() {
|
|
136
|
-
return noInstall;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const answers = await prompt(questions);
|
|
142
|
-
return {
|
|
143
|
-
projectName: projectName || answers.projectName,
|
|
144
|
-
appType: answers.appType,
|
|
145
|
-
port: Number(answers.port),
|
|
146
|
-
install: noInstall ? false : Boolean(answers.install)
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function serverJsTemplate({ port, appType }) {
|
|
151
|
-
const staticRoot = appType === 'both' ? 'web' : '.';
|
|
152
|
-
|
|
153
|
-
return `
|
|
154
|
-
`const
|
|
155
|
-
`const
|
|
156
|
-
`
|
|
157
|
-
`
|
|
158
|
-
`
|
|
159
|
-
`
|
|
160
|
-
`
|
|
161
|
-
`
|
|
162
|
-
`
|
|
163
|
-
`
|
|
164
|
-
`
|
|
165
|
-
`
|
|
166
|
-
|
|
167
|
-
`
|
|
168
|
-
|
|
169
|
-
`
|
|
170
|
-
`
|
|
171
|
-
`
|
|
172
|
-
`
|
|
173
|
-
`
|
|
174
|
-
`
|
|
175
|
-
`
|
|
176
|
-
`
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
`
|
|
182
|
-
`
|
|
183
|
-
`
|
|
184
|
-
`
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`
|
|
190
|
-
`
|
|
191
|
-
`
|
|
192
|
-
`
|
|
193
|
-
`
|
|
194
|
-
`
|
|
195
|
-
`
|
|
196
|
-
`
|
|
197
|
-
`
|
|
198
|
-
` });\n
|
|
199
|
-
`
|
|
200
|
-
`
|
|
201
|
-
`
|
|
202
|
-
`
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
`
|
|
208
|
-
`
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
deps
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
await
|
|
392
|
-
await fs.writeFile(path.join(targetDir, 'server.js'), serverJsTemplate({ port, appType }), 'utf8');
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
await fs.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
'
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
await fs.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
);
|
|
425
|
-
await fs.writeFile(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
'
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import enquirer from 'enquirer';
|
|
12
|
+
|
|
13
|
+
const { prompt } = enquirer;
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
function printHelp() {
|
|
20
|
+
console.log(`\n${chalk.bold('create-switch-framework-app')}\n`);
|
|
21
|
+
console.log('Usage:');
|
|
22
|
+
console.log(' npx create-switch-framework-app <project-name> [options]');
|
|
23
|
+
console.log('\nOptions:');
|
|
24
|
+
console.log(' --yes, -y Skip prompts and use defaults');
|
|
25
|
+
console.log(' --app-type One of: web | electron | both');
|
|
26
|
+
console.log(' --port Server port (1-65535)');
|
|
27
|
+
console.log(' --no-install Do not run npm install');
|
|
28
|
+
console.log(' --use-local Use npm link for switch-framework + switch-framework-backend (no npm registry)');
|
|
29
|
+
console.log(' -h, --help Show help');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const args = argv.slice(2);
|
|
34
|
+
const flags = new Set(args.filter(a => a.startsWith('-')));
|
|
35
|
+
|
|
36
|
+
const help = flags.has('-h') || flags.has('--help');
|
|
37
|
+
const yes = flags.has('--yes') || flags.has('-y');
|
|
38
|
+
const noInstall = flags.has('--no-install');
|
|
39
|
+
const useLocal = flags.has('--use-local');
|
|
40
|
+
|
|
41
|
+
const getValue = (name) => {
|
|
42
|
+
const i = args.indexOf(name);
|
|
43
|
+
if (i === -1) return null;
|
|
44
|
+
const v = args[i + 1];
|
|
45
|
+
if (!v || v.startsWith('-')) return null;
|
|
46
|
+
return v;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const appType = getValue('--app-type');
|
|
50
|
+
const port = getValue('--port');
|
|
51
|
+
|
|
52
|
+
const positional = args.filter(a => !a.startsWith('-'));
|
|
53
|
+
const projectName = positional[0] || null;
|
|
54
|
+
|
|
55
|
+
return { help, yes, noInstall, useLocal, projectName, appType, port };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sanitizeProjectName(input) {
|
|
59
|
+
const name = String(input || '').trim();
|
|
60
|
+
return name.replace(/[\\/]/g, '-');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toPackageName(projectName) {
|
|
64
|
+
return projectName
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/\s+/g, '-')
|
|
67
|
+
.replace(/[^a-z0-9-_]/g, '-')
|
|
68
|
+
.replace(/-+/g, '-')
|
|
69
|
+
.replace(/^-+|-+$/g, '') || 'switch-framework-app';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function askQuestions({ projectName, yes, noInstall, appTypeOverride, portOverride }) {
|
|
73
|
+
const defaults = {
|
|
74
|
+
appType: 'web',
|
|
75
|
+
port: 3000,
|
|
76
|
+
install: true
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const resolvedAppType = appTypeOverride || defaults.appType;
|
|
80
|
+
const resolvedPort = portOverride != null ? Number(portOverride) : defaults.port;
|
|
81
|
+
|
|
82
|
+
if (yes) {
|
|
83
|
+
return {
|
|
84
|
+
projectName: projectName || 'switch-framework-app',
|
|
85
|
+
appType: resolvedAppType,
|
|
86
|
+
port: resolvedPort,
|
|
87
|
+
install: noInstall ? false : defaults.install
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const questions = [];
|
|
92
|
+
|
|
93
|
+
if (!projectName) {
|
|
94
|
+
questions.push({
|
|
95
|
+
type: 'input',
|
|
96
|
+
name: 'projectName',
|
|
97
|
+
message: 'Project name',
|
|
98
|
+
initial: 'switch-framework-app',
|
|
99
|
+
validate(value) {
|
|
100
|
+
const v = sanitizeProjectName(value);
|
|
101
|
+
if (!v) return 'Project name is required';
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
questions.push(
|
|
108
|
+
{
|
|
109
|
+
type: 'select',
|
|
110
|
+
name: 'appType',
|
|
111
|
+
message: 'What type of app do you want to create?',
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: 'web', message: 'Web App (browser + Node.js/Express backend)' },
|
|
114
|
+
{ name: 'electron', message: 'Electron Desktop App (with shared Express backend)' },
|
|
115
|
+
{ name: 'both', message: 'Both (monorepo with web + electron targets)' }
|
|
116
|
+
],
|
|
117
|
+
initial: resolvedAppType
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'numeral',
|
|
121
|
+
name: 'port',
|
|
122
|
+
message: 'Which port do you want the server to use?',
|
|
123
|
+
initial: resolvedPort,
|
|
124
|
+
validate(value) {
|
|
125
|
+
const n = Number(value);
|
|
126
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) return 'Enter a valid port (1-65535)';
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'confirm',
|
|
132
|
+
name: 'install',
|
|
133
|
+
message: 'Do you want to install dependencies automatically?',
|
|
134
|
+
initial: defaults.install,
|
|
135
|
+
skip() {
|
|
136
|
+
return noInstall;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const answers = await prompt(questions);
|
|
142
|
+
return {
|
|
143
|
+
projectName: projectName || answers.projectName,
|
|
144
|
+
appType: answers.appType,
|
|
145
|
+
port: Number(answers.port),
|
|
146
|
+
install: noInstall ? false : Boolean(answers.install)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function serverJsTemplate({ port, appType }) {
|
|
151
|
+
const staticRoot = appType === 'both' ? 'web' : '.';
|
|
152
|
+
|
|
153
|
+
return `require('dotenv').config();\n\n` +
|
|
154
|
+
`const path = require('node:path');\n` +
|
|
155
|
+
`const switchFrameworkBackend = require('switch-framework-backend');\n\n` +
|
|
156
|
+
`switchFrameworkBackend.config({\n` +
|
|
157
|
+
` PORT: process.env.PORT ? Number(process.env.PORT) : ${port},\n` +
|
|
158
|
+
` staticRoot: path.join(__dirname, '${staticRoot}'),\n` +
|
|
159
|
+
` session: {\n` +
|
|
160
|
+
` secret: process.env.SESSION_SECRET || 'dev-secret',\n` +
|
|
161
|
+
` resave: false,\n` +
|
|
162
|
+
` saveUninitialized: false\n` +
|
|
163
|
+
` }\n` +
|
|
164
|
+
`});\n\n` +
|
|
165
|
+
`const app = switchFrameworkBackend();\n\n` +
|
|
166
|
+
`app.initServer((server) => {\n` +
|
|
167
|
+
` const restrictConfig = {\n` +
|
|
168
|
+
` public: ['/', '/login'],\n` +
|
|
169
|
+
` rules: [\n` +
|
|
170
|
+
` { prefix: '/admin', roles: ['admin'] },\n` +
|
|
171
|
+
` { prefix: '/billing', roles: ['billing', 'admin'] },\n` +
|
|
172
|
+
` { path: '/login', roles: ['*'] }\n` +
|
|
173
|
+
` ]\n` +
|
|
174
|
+
` };\n` +
|
|
175
|
+
` server.use(switchFrameworkBackend.checkRestrict(restrictConfig));\n` +
|
|
176
|
+
`});\n`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function electronMainTemplate({ port }) {
|
|
180
|
+
return `const { app, BrowserWindow } = require('electron');\n` +
|
|
181
|
+
`const path = require('node:path');\n\n` +
|
|
182
|
+
`let mainWindow;\n\n` +
|
|
183
|
+
`function createWindow() {\n` +
|
|
184
|
+
` mainWindow = new BrowserWindow({\n` +
|
|
185
|
+
` width: 1200,\n` +
|
|
186
|
+
` height: 800,\n` +
|
|
187
|
+
` webPreferences: {\n` +
|
|
188
|
+
` preload: path.join(__dirname, 'preload.js')\n` +
|
|
189
|
+
` }\n` +
|
|
190
|
+
` });\n\n` +
|
|
191
|
+
` mainWindow.loadURL('http://localhost:${port}');\n` +
|
|
192
|
+
`}\n\n` +
|
|
193
|
+
`app.whenReady().then(() => {\n` +
|
|
194
|
+
` require('../server.js');\n` +
|
|
195
|
+
` setTimeout(createWindow, 350);\n` +
|
|
196
|
+
` app.on('activate', () => {\n` +
|
|
197
|
+
` if (BrowserWindow.getAllWindows().length === 0) createWindow();\n` +
|
|
198
|
+
` });\n` +
|
|
199
|
+
`});\n\n` +
|
|
200
|
+
`app.on('window-all-closed', () => {\n` +
|
|
201
|
+
` if (process.platform !== 'darwin') app.quit();\n` +
|
|
202
|
+
`});\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function electronPreloadTemplate() {
|
|
206
|
+
return `const { contextBridge } = require('electron');\n\n` +
|
|
207
|
+
`contextBridge.exposeInMainWorld('switchApp', {\n` +
|
|
208
|
+
` ping: () => 'pong'\n` +
|
|
209
|
+
`});\n`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function electronBuilderTemplate({ packageName }) {
|
|
213
|
+
return JSON.stringify(
|
|
214
|
+
{
|
|
215
|
+
appId: `com.switchframework.${packageName}`,
|
|
216
|
+
productName: packageName,
|
|
217
|
+
directories: { output: 'dist' },
|
|
218
|
+
files: ['**/*'],
|
|
219
|
+
win: { target: 'nsis' },
|
|
220
|
+
mac: { target: 'dmg' },
|
|
221
|
+
linux: { target: 'AppImage' }
|
|
222
|
+
},
|
|
223
|
+
null,
|
|
224
|
+
2
|
|
225
|
+
) + '\n';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createPackageJson({ packageName, appType, port, useLocal }) {
|
|
229
|
+
const scripts = {
|
|
230
|
+
dev: 'node server.js',
|
|
231
|
+
start: 'node server.js'
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (appType === 'electron' || appType === 'both') {
|
|
235
|
+
scripts['electron:dev'] = 'electron .';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const deps = {
|
|
239
|
+
dotenv: '^16.4.5',
|
|
240
|
+
express: '^4.19.2',
|
|
241
|
+
'express-session': '^1.18.1'
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// When --use-local is set, we intentionally do NOT add switch-framework deps to package.json
|
|
245
|
+
// to avoid npm registry fetching during testing. We will npm link them instead.
|
|
246
|
+
if (!useLocal) {
|
|
247
|
+
deps['switch-framework'] = '^0.2.0';
|
|
248
|
+
deps['switch-framework-backend'] = '^0.2.0';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const pkg = {
|
|
252
|
+
name: packageName,
|
|
253
|
+
private: true,
|
|
254
|
+
type: 'commonjs',
|
|
255
|
+
scripts,
|
|
256
|
+
dependencies: deps
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (appType === 'electron' || appType === 'both') {
|
|
260
|
+
pkg.main = 'main.js';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (appType === 'electron' || appType === 'both') {
|
|
264
|
+
pkg.devDependencies = {
|
|
265
|
+
electron: 'latest',
|
|
266
|
+
'electron-builder': 'latest'
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Keep port discoverable
|
|
271
|
+
pkg.switchFramework = { port };
|
|
272
|
+
|
|
273
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function copyDir(srcDir, destDir) {
|
|
277
|
+
await fs.ensureDir(destDir);
|
|
278
|
+
await fs.copy(srcDir, destDir, {
|
|
279
|
+
overwrite: true,
|
|
280
|
+
errorOnExist: false
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function runNpmInstall({ cwd }) {
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const child = require('node:child_process').spawn('npm', ['install'], {
|
|
287
|
+
cwd,
|
|
288
|
+
stdio: 'inherit',
|
|
289
|
+
shell: process.platform === 'win32'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
child.on('exit', (code) => {
|
|
293
|
+
if (code === 0) resolve();
|
|
294
|
+
else reject(new Error(`npm install failed with exit code ${code}`));
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
child.on('error', reject);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function runNpmLink({ cwd, packages }) {
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
const child = require('node:child_process').spawn('npm', ['link', ...packages], {
|
|
304
|
+
cwd,
|
|
305
|
+
stdio: 'inherit',
|
|
306
|
+
shell: process.platform === 'win32'
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
child.on('exit', (code) => {
|
|
310
|
+
if (code === 0) resolve();
|
|
311
|
+
else reject(new Error(`npm link failed with exit code ${code}`));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
child.on('error', reject);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const DOCS_URL = 'https://github.com/Switcherfaiz/switch-framework-docs';
|
|
319
|
+
|
|
320
|
+
function getCliVersion() {
|
|
321
|
+
try {
|
|
322
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
323
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
324
|
+
return pkg.version || '0.0.0';
|
|
325
|
+
} catch {
|
|
326
|
+
return '0.0.0';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function main() {
|
|
331
|
+
const cliVersion = getCliVersion();
|
|
332
|
+
console.log(chalk.cyan(`\nWelcome to switch-framework CLI v${cliVersion}\n`));
|
|
333
|
+
|
|
334
|
+
const { help, yes, noInstall, useLocal, appType: appTypeArg, port: portArg, projectName: rawProjectName } = parseArgs(process.argv);
|
|
335
|
+
if (help) {
|
|
336
|
+
printHelp();
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const normalizedAppType = appTypeArg ? String(appTypeArg).trim().toLowerCase() : null;
|
|
341
|
+
if (normalizedAppType && !['web', 'electron', 'both'].includes(normalizedAppType)) {
|
|
342
|
+
console.error(chalk.red(`Invalid --app-type: ${appTypeArg}. Use one of: web | electron | both`));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const normalizedPort = portArg != null ? Number(portArg) : null;
|
|
347
|
+
if (portArg != null && (!Number.isInteger(normalizedPort) || normalizedPort < 1 || normalizedPort > 65535)) {
|
|
348
|
+
console.error(chalk.red(`Invalid --port: ${portArg}. Use an integer 1-65535`));
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const answers = await askQuestions({
|
|
353
|
+
projectName: rawProjectName,
|
|
354
|
+
yes,
|
|
355
|
+
noInstall,
|
|
356
|
+
appTypeOverride: normalizedAppType,
|
|
357
|
+
portOverride: normalizedPort
|
|
358
|
+
});
|
|
359
|
+
const projectName = sanitizeProjectName(answers.projectName);
|
|
360
|
+
const appType = answers.appType;
|
|
361
|
+
const port = answers.port;
|
|
362
|
+
const install = answers.install;
|
|
363
|
+
|
|
364
|
+
if (!projectName) {
|
|
365
|
+
console.error(chalk.red('Project name is required.'));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
370
|
+
const spinner = ora();
|
|
371
|
+
|
|
372
|
+
const templatesRoot = path.resolve(__dirname, '..', 'templates');
|
|
373
|
+
const webBase = path.join(templatesRoot, 'web', 'base');
|
|
374
|
+
const electronBase = path.join(templatesRoot, 'electron', 'base');
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
if (await fs.pathExists(targetDir)) {
|
|
378
|
+
const items = await fs.readdir(targetDir);
|
|
379
|
+
if (items.length > 0) {
|
|
380
|
+
console.error(chalk.red(`Target directory already exists and is not empty: ${targetDir}`));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
spinner.start('Creating project...');
|
|
386
|
+
await fs.ensureDir(targetDir);
|
|
387
|
+
|
|
388
|
+
const packageName = toPackageName(projectName);
|
|
389
|
+
|
|
390
|
+
if (appType === 'web') {
|
|
391
|
+
await copyDir(webBase, targetDir);
|
|
392
|
+
await fs.writeFile(path.join(targetDir, 'server.js'), serverJsTemplate({ port, appType }), 'utf8');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (appType === 'electron') {
|
|
396
|
+
const baseDir = (await fs.pathExists(electronBase)) ? electronBase : webBase;
|
|
397
|
+
await copyDir(baseDir, targetDir);
|
|
398
|
+
await fs.ensureDir(path.join(targetDir, 'electron'));
|
|
399
|
+
await fs.writeFile(path.join(targetDir, 'server.js'), serverJsTemplate({ port, appType }), 'utf8');
|
|
400
|
+
await fs.writeFile(path.join(targetDir, 'electron', 'main.js'), electronMainTemplate({ port }), 'utf8');
|
|
401
|
+
await fs.writeFile(path.join(targetDir, 'electron', 'preload.js'), electronPreloadTemplate(), 'utf8');
|
|
402
|
+
await fs.writeFile(path.join(targetDir, 'electron', 'electron-builder.json'), electronBuilderTemplate({ packageName }), 'utf8');
|
|
403
|
+
|
|
404
|
+
// Electron expects entry at project root
|
|
405
|
+
await fs.writeFile(
|
|
406
|
+
path.join(targetDir, 'main.js'),
|
|
407
|
+
"module.exports = require('./electron/main.js');\n",
|
|
408
|
+
'utf8'
|
|
409
|
+
);
|
|
410
|
+
await fs.writeFile(
|
|
411
|
+
path.join(targetDir, 'preload.js'),
|
|
412
|
+
"module.exports = require('./electron/preload.js');\n",
|
|
413
|
+
'utf8'
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (appType === 'both') {
|
|
418
|
+
await fs.ensureDir(path.join(targetDir, 'web'));
|
|
419
|
+
await copyDir(webBase, path.join(targetDir, 'web'));
|
|
420
|
+
|
|
421
|
+
await fs.ensureDir(path.join(targetDir, 'electron'));
|
|
422
|
+
await fs.writeFile(path.join(targetDir, 'server.js'), serverJsTemplate({ port, appType }), 'utf8');
|
|
423
|
+
await fs.writeFile(path.join(targetDir, 'electron', 'main.js'), electronMainTemplate({ port }), 'utf8');
|
|
424
|
+
await fs.writeFile(path.join(targetDir, 'electron', 'preload.js'), electronPreloadTemplate(), 'utf8');
|
|
425
|
+
await fs.writeFile(path.join(targetDir, 'electron', 'electron-builder.json'), electronBuilderTemplate({ packageName }), 'utf8');
|
|
426
|
+
|
|
427
|
+
await fs.writeFile(
|
|
428
|
+
path.join(targetDir, 'main.js'),
|
|
429
|
+
"module.exports = require('./electron/main.js');\n",
|
|
430
|
+
'utf8'
|
|
431
|
+
);
|
|
432
|
+
await fs.writeFile(
|
|
433
|
+
path.join(targetDir, 'preload.js'),
|
|
434
|
+
"module.exports = require('./electron/preload.js');\n",
|
|
435
|
+
'utf8'
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
await fs.writeFile(
|
|
440
|
+
path.join(targetDir, '.env.example'),
|
|
441
|
+
`PORT=${port}\nSESSION_SECRET=dev-secret\n`,
|
|
442
|
+
'utf8'
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
await fs.writeFile(
|
|
446
|
+
path.join(targetDir, 'package.json'),
|
|
447
|
+
createPackageJson({ packageName, appType, port, useLocal }),
|
|
448
|
+
'utf8'
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
spinner.succeed('Project created');
|
|
452
|
+
|
|
453
|
+
if (install) {
|
|
454
|
+
spinner.start('Installing dependencies (npm install)...');
|
|
455
|
+
try {
|
|
456
|
+
await runNpmInstall({ cwd: targetDir });
|
|
457
|
+
spinner.succeed('Dependencies installed');
|
|
458
|
+
} catch (e) {
|
|
459
|
+
spinner.warn('npm install failed (project was still created)');
|
|
460
|
+
console.error(chalk.yellow(e?.message || String(e)));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (useLocal) {
|
|
465
|
+
spinner.start('Linking local packages (npm link switch-framework switch-framework-backend)...');
|
|
466
|
+
try {
|
|
467
|
+
await runNpmLink({ cwd: targetDir, packages: ['switch-framework', 'switch-framework-backend'] });
|
|
468
|
+
spinner.succeed('Local packages linked');
|
|
469
|
+
} catch (e) {
|
|
470
|
+
spinner.warn('npm link failed');
|
|
471
|
+
console.error(chalk.yellow(e?.message || String(e)));
|
|
472
|
+
console.log(chalk.yellow('Make sure you ran npm link inside your switch-framework and switch-framework-backend packages first.'));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Ensure no spinner state bleeds into final output
|
|
477
|
+
spinner.stop();
|
|
478
|
+
|
|
479
|
+
console.log('\n' + chalk.green(chalk.bold('Success!')));
|
|
480
|
+
console.log('\nNext steps:');
|
|
481
|
+
console.log(' ' + chalk.cyan('cd ' + projectName));
|
|
482
|
+
|
|
483
|
+
if (!install) {
|
|
484
|
+
console.log(' ' + chalk.cyan('npm install'));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (useLocal) {
|
|
488
|
+
console.log(' ' + chalk.cyan('npm link switch-framework switch-framework-backend'));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (appType === 'web') {
|
|
492
|
+
console.log(' ' + chalk.cyan('npm run dev'));
|
|
493
|
+
} else if (appType === 'electron') {
|
|
494
|
+
console.log(' ' + chalk.cyan('npm run electron:dev'));
|
|
495
|
+
} else {
|
|
496
|
+
console.log(' ' + chalk.cyan('npm run dev'));
|
|
497
|
+
console.log(' ' + chalk.cyan('npm run electron:dev'));
|
|
498
|
+
console.log('\nNote: web UI lives in ./web and electron files in ./electron');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
console.log('\n' + chalk.gray('Read the docs: ') + chalk.cyan(DOCS_URL));
|
|
502
|
+
console.log('');
|
|
503
|
+
} catch (err) {
|
|
504
|
+
spinner.fail('Failed');
|
|
505
|
+
console.error(chalk.red(err?.stack || err?.message || String(err)));
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
main();
|