flutterbridge 1.0.1

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +510 -0
  3. package/package.json +40 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flutter Bridge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,510 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync, rmSync } from 'fs';
4
+ import path from 'path';
5
+ import { execSync } from 'child_process';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import inquirer from 'inquirer';
9
+ const program = new Command();
10
+ // Configuração da CLI
11
+ program
12
+ .name('flutterbridge')
13
+ .description('CLI para transformar SPAs em apps mobile usando Flutter')
14
+ .version('1.0.0');
15
+ // Comando: init
16
+ program
17
+ .command('init')
18
+ .description('Inicializa um novo projeto FlutterBridge')
19
+ .argument('[project-name]', 'Nome do projeto')
20
+ .action(async (projectName) => {
21
+ try {
22
+ const answers = await inquirer.prompt([
23
+ {
24
+ type: 'input',
25
+ name: 'name',
26
+ message: 'Nome do projeto:',
27
+ default: projectName || 'my-app',
28
+ validate: (input) => input.length > 0 || 'Nome é obrigatório'
29
+ },
30
+ {
31
+ type: 'input',
32
+ name: 'appId',
33
+ message: 'App ID (ex: com.example.app):',
34
+ default: 'com.example.app',
35
+ validate: (input) => /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(input) || 'App ID inválido'
36
+ },
37
+ {
38
+ type: 'input',
39
+ name: 'appName',
40
+ message: 'Nome do app (exibido no dispositivo):',
41
+ default: (answers) => answers.name
42
+ },
43
+ {
44
+ type: 'list',
45
+ name: 'webDir',
46
+ message: 'Diretório de build do seu projeto web:',
47
+ choices: ['dist', 'build', 'public', 'www', 'outro'],
48
+ },
49
+ {
50
+ type: 'input',
51
+ name: 'customWebDir',
52
+ message: 'Digite o diretório customizado:',
53
+ when: (answers) => answers.webDir === 'outro'
54
+ }
55
+ ]);
56
+ const webDir = answers.customWebDir || answers.webDir;
57
+ await initProject(answers.name, answers.appId, answers.appName, webDir);
58
+ }
59
+ catch (error) {
60
+ console.error(chalk.red('Erro ao inicializar projeto:'), error);
61
+ }
62
+ });
63
+ // Comando: add
64
+ program
65
+ .command('add')
66
+ .description('Adiciona uma plataforma (android/ios)')
67
+ .argument('<platform>', 'Plataforma: android ou ios')
68
+ .action(async (platform) => {
69
+ await addPlatform(platform);
70
+ });
71
+ // Comando: sync
72
+ program
73
+ .command('sync')
74
+ .description('Sincroniza o projeto web com o Flutter')
75
+ .action(async () => {
76
+ await syncProject();
77
+ });
78
+ // Comando: run
79
+ program
80
+ .command('run')
81
+ .description('Executa o app em um dispositivo/emulador')
82
+ .argument('<platform>', 'Plataforma: android ou ios')
83
+ .option('-d, --device <device>', 'ID do dispositivo específico')
84
+ .action(async (platform, options) => {
85
+ await runApp(platform, options.device);
86
+ });
87
+ // Comando: build
88
+ program
89
+ .command('build')
90
+ .description('Faz build do app')
91
+ .argument('<platform>', 'Plataforma: android ou ios')
92
+ .option('--release', 'Build de produção')
93
+ .action(async (platform, options) => {
94
+ await buildApp(platform, options.release);
95
+ });
96
+ // Comando: plugin
97
+ program
98
+ .command('plugin')
99
+ .description('Gerencia plugins nativos')
100
+ .argument('<action>', 'add ou list')
101
+ .argument('[plugin]', 'Nome do plugin (para add)')
102
+ .action(async (action, plugin) => {
103
+ if (action === 'add' && plugin) {
104
+ await addPlugin(plugin);
105
+ }
106
+ else if (action === 'list') {
107
+ await listPlugins();
108
+ }
109
+ });
110
+ // Funções auxiliares
111
+ async function initProject(name, appId, appName, webDir) {
112
+ const spinner = ora('Criando projeto FlutterBridge...').start();
113
+ try {
114
+ // Verifica se Flutter está instalado
115
+ try {
116
+ execSync('flutter --version', { stdio: 'ignore' });
117
+ }
118
+ catch {
119
+ spinner.fail('Flutter não encontrado. Instale o Flutter primeiro.');
120
+ return;
121
+ }
122
+ const projectPath = path.join(process.cwd(), name);
123
+ // Cria estrutura de diretórios
124
+ mkdirSync(projectPath, { recursive: true });
125
+ mkdirSync(path.join(projectPath, 'flutter_app'), { recursive: true });
126
+ // Cria flutterbridge.config.json
127
+ const config = {
128
+ appId,
129
+ appName,
130
+ webDir,
131
+ flutter: {
132
+ version: '3.0.0',
133
+ projectPath: 'flutter_app'
134
+ },
135
+ plugins: []
136
+ };
137
+ writeFileSync(path.join(projectPath, 'flutterbridge.config.json'), JSON.stringify(config, null, 2));
138
+ // Cria projeto Flutter
139
+ spinner.text = 'Criando projeto Flutter...';
140
+ execSync(`flutter create --org ${appId.split('.').slice(0, -1).join('.')} --project-name ${name.replace(/-/g, '_')} flutter_app`, {
141
+ cwd: projectPath,
142
+ stdio: 'pipe'
143
+ });
144
+ // Atualiza pubspec.yaml
145
+ const pubspecPath = path.join(projectPath, 'flutter_app', 'pubspec.yaml');
146
+ let pubspec = readFileSync(pubspecPath, 'utf-8');
147
+ // Adiciona dependências
148
+ // Encontra onde está "dependencies:" e adiciona depois
149
+ pubspec = pubspec.replace('dependencies:', `dependencies:
150
+ webview_flutter: ^4.4.2
151
+ path_provider: ^2.1.1
152
+ flutter_inappwebview: ^6.0.0`);
153
+ writeFileSync(pubspecPath, pubspec);
154
+ // Cria main.dart customizado
155
+ const mainDart = generateMainDart(appName);
156
+ writeFileSync(path.join(projectPath, 'flutter_app', 'lib', 'main.dart'), mainDart);
157
+ // Cria diretório para assets web
158
+ mkdirSync(path.join(projectPath, 'flutter_app', 'assets', 'web'), { recursive: true });
159
+ // Atualiza pubspec.yaml para incluir assets
160
+ pubspec += `
161
+ assets:
162
+ - assets/web/
163
+ `;
164
+ writeFileSync(pubspecPath, pubspec);
165
+ // Cria README
166
+ const readme = generateReadme(name, webDir);
167
+ writeFileSync(path.join(projectPath, 'README.md'), readme);
168
+ spinner.succeed(chalk.green(`Projeto ${name} criado com sucesso!`));
169
+ console.log(chalk.cyan('\nPróximos passos:'));
170
+ console.log(chalk.white(` cd ${name}`));
171
+ console.log(chalk.white(` # Faça o build do seu projeto web para ./${webDir}`));
172
+ console.log(chalk.white(` flutterbridge sync`));
173
+ console.log(chalk.white(` flutterbridge run android`));
174
+ }
175
+ catch (error) {
176
+ spinner.fail('Erro ao criar projeto');
177
+ throw error;
178
+ }
179
+ }
180
+ async function addPlatform(platform) {
181
+ if (!['android', 'ios'].includes(platform)) {
182
+ console.error(chalk.red('Plataforma deve ser android ou ios'));
183
+ return;
184
+ }
185
+ const spinner = ora(`Adicionando plataforma ${platform}...`).start();
186
+ try {
187
+ const config = await loadConfig();
188
+ const flutterPath = path.join(process.cwd(), config.flutter.projectPath);
189
+ // Flutter já cria android e ios por padrão, mas podemos recriar se necessário
190
+ console.log(chalk.green(`Plataforma ${platform} já está disponível no projeto Flutter`));
191
+ spinner.succeed();
192
+ }
193
+ catch (error) {
194
+ spinner.fail('Erro ao adicionar plataforma');
195
+ throw error;
196
+ }
197
+ }
198
+ async function syncProject() {
199
+ const spinner = ora('Sincronizando projeto...').start();
200
+ try {
201
+ const config = await loadConfig();
202
+ const webDirPath = path.join(process.cwd(), config.webDir);
203
+ const assetsPath = path.join(process.cwd(), config.flutter.projectPath, 'assets', 'web');
204
+ // Verifica se diretório web existe
205
+ if (!existsSync(webDirPath)) {
206
+ spinner.fail(`Diretório ${config.webDir} não encontrado. Execute o build do seu projeto web primeiro.`);
207
+ return;
208
+ }
209
+ // Copia arquivos web para assets do Flutter
210
+ // limpa e copia (síncrono)
211
+ rmSync(assetsPath, { recursive: true, force: true });
212
+ mkdirSync(assetsPath, { recursive: true });
213
+ cpSync(webDirPath, assetsPath, { recursive: true });
214
+ // Executa flutter pub get
215
+ spinner.text = 'Instalando dependências Flutter...';
216
+ execSync('flutter pub get', {
217
+ cwd: path.join(process.cwd(), config.flutter.projectPath),
218
+ stdio: 'pipe'
219
+ });
220
+ spinner.succeed(chalk.green('Projeto sincronizado com sucesso!'));
221
+ }
222
+ catch (error) {
223
+ spinner.fail('Erro ao sincronizar projeto');
224
+ throw error;
225
+ }
226
+ }
227
+ async function runApp(platform, device) {
228
+ const spinner = ora(`Executando app em ${platform}...`).start();
229
+ try {
230
+ await syncProject();
231
+ const config = await loadConfig();
232
+ const flutterPath = path.join(process.cwd(), config.flutter.projectPath);
233
+ const deviceFlag = device ? `-d ${device}` : '';
234
+ const cmd = `flutter run ${deviceFlag}`;
235
+ spinner.stop();
236
+ console.log(chalk.cyan(`Executando: ${cmd}`));
237
+ execSync(cmd, {
238
+ cwd: flutterPath,
239
+ stdio: 'inherit'
240
+ });
241
+ }
242
+ catch (error) {
243
+ spinner.fail('Erro ao executar app');
244
+ throw error;
245
+ }
246
+ }
247
+ async function buildApp(platform, release = false) {
248
+ const spinner = ora(`Fazendo build para ${platform}...`).start();
249
+ try {
250
+ await syncProject();
251
+ const config = await loadConfig();
252
+ const flutterPath = path.join(process.cwd(), config.flutter.projectPath);
253
+ const buildMode = release ? '--release' : '--debug';
254
+ const cmd = `flutter build ${platform === 'ios' ? 'ios' : 'apk'} ${buildMode}`;
255
+ spinner.text = `Executando: ${cmd}`;
256
+ execSync(cmd, {
257
+ cwd: flutterPath,
258
+ stdio: 'inherit'
259
+ });
260
+ spinner.succeed(chalk.green(`Build concluído!`));
261
+ if (platform === 'android') {
262
+ const apkPath = path.join(flutterPath, 'build', 'app', 'outputs', 'flutter-apk', release ? 'app-release.apk' : 'app-debug.apk');
263
+ console.log(chalk.cyan(`APK disponível em: ${apkPath}`));
264
+ }
265
+ }
266
+ catch (error) {
267
+ spinner.fail('Erro ao fazer build');
268
+ throw error;
269
+ }
270
+ }
271
+ async function addPlugin(plugin) {
272
+ const spinner = ora(`Adicionando plugin ${plugin}...`).start();
273
+ try {
274
+ const config = await loadConfig();
275
+ const flutterPath = path.join(process.cwd(), config.flutter.projectPath);
276
+ execSync(`flutter pub add ${plugin}`, {
277
+ cwd: flutterPath,
278
+ stdio: 'pipe'
279
+ });
280
+ // Atualiza config
281
+ config.plugins.push(plugin);
282
+ writeFileSync(path.join(process.cwd(), 'flutterbridge.config.json'), JSON.stringify(config, null, 2));
283
+ spinner.succeed(chalk.green(`Plugin ${plugin} adicionado!`));
284
+ }
285
+ catch (error) {
286
+ spinner.fail('Erro ao adicionar plugin');
287
+ throw error;
288
+ }
289
+ }
290
+ async function listPlugins() {
291
+ try {
292
+ const config = await loadConfig();
293
+ if (config.plugins.length === 0) {
294
+ console.log(chalk.yellow('Nenhum plugin instalado'));
295
+ return;
296
+ }
297
+ console.log(chalk.cyan('Plugins instalados:'));
298
+ config.plugins.forEach((plugin) => {
299
+ console.log(chalk.white(` - ${plugin}`));
300
+ });
301
+ }
302
+ catch (error) {
303
+ console.error(chalk.red('Erro ao listar plugins'));
304
+ }
305
+ }
306
+ async function loadConfig() {
307
+ const configPath = path.join(process.cwd(), 'flutterbridge.config.json');
308
+ if (!existsSync(configPath)) {
309
+ throw new Error('Arquivo flutterbridge.config.json não encontrado. Execute "flutterbridge init" primeiro.');
310
+ }
311
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
312
+ }
313
+ function generateMainDart(appName) {
314
+ return `import 'package:flutter/material.dart';
315
+ import 'package:flutter/foundation.dart';
316
+ import 'package:flutter/services.dart';
317
+ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
318
+
319
+ void main() {
320
+ runApp(MyApp());
321
+ }
322
+
323
+ class MyApp extends StatelessWidget {
324
+ @override
325
+ Widget build(BuildContext context) {
326
+ return MaterialApp(
327
+ title: '${appName}',
328
+ theme: ThemeData(primarySwatch: Colors.blue),
329
+ home: WebViewPage(),
330
+ );
331
+ }
332
+ }
333
+
334
+ class WebViewPage extends StatefulWidget {
335
+ @override
336
+ _WebViewPageState createState() => _WebViewPageState();
337
+ }
338
+
339
+ class _WebViewPageState extends State<WebViewPage> {
340
+ InAppWebViewController? webViewController;
341
+ double progress = 0;
342
+
343
+ @override
344
+ Widget build(BuildContext context) {
345
+ return Scaffold(
346
+ body: SafeArea(
347
+ child: Column(
348
+ children: [
349
+ progress < 1.0
350
+ ? LinearProgressIndicator(value: progress)
351
+ : Container(),
352
+ Expanded(
353
+ child: InAppWebView(
354
+ initialSettings: InAppWebViewSettings(
355
+ javaScriptEnabled: true,
356
+ allowFileAccessFromFileURLs: true,
357
+ allowUniversalAccessFromFileURLs: true,
358
+ mediaPlaybackRequiresUserGesture: false,
359
+ useOnLoadResource: true,
360
+ ),
361
+ initialFile: 'assets/web/index.html',
362
+ onWebViewCreated: (controller) {
363
+ webViewController = controller;
364
+
365
+ if (!kIsWeb) {
366
+ controller.addJavaScriptHandler(
367
+ handlerName: 'FlutterBridge',
368
+ callback: (args) {
369
+ print('Mensagem do JS: \$args');
370
+ return {'response': 'Recebido pelo Flutter', 'data': args};
371
+ },
372
+ );
373
+ }
374
+ },
375
+ onProgressChanged: (controller, progress) {
376
+ setState(() {
377
+ this.progress = progress / 100;
378
+ });
379
+ },
380
+ onLoadStop: (controller, url) {
381
+ setState(() {
382
+ progress = 1.0;
383
+ });
384
+ },
385
+ onLoadResource: (controller, resource) {
386
+ print('Carregando: \${resource.url}');
387
+ },
388
+ onLoadResourceWithCustomScheme: (controller, request) async {
389
+ // Intercepta requests personalizados
390
+ var url = request.url.toString();
391
+ print('Custom scheme: \$url');
392
+
393
+ return null;
394
+ },
395
+ shouldOverrideUrlLoading: (controller, navigationAction) async {
396
+ var uri = navigationAction.request.url!;
397
+ var url = uri.toString();
398
+
399
+ print('Navegando para: \$url');
400
+
401
+ // Se for navegação interna (mesma origem), permite
402
+ if (url.startsWith('file://') ||
403
+ url.startsWith('about:') ||
404
+ url.contains('assets/web/')) {
405
+ return NavigationActionPolicy.ALLOW;
406
+ }
407
+
408
+ // Se for link externo (http/https), abre no navegador
409
+ if (url.startsWith('http://') || url.startsWith('https://')) {
410
+ // Aqui você pode usar url_launcher se quiser abrir no browser
411
+ print('Link externo detectado: \$url');
412
+ return NavigationActionPolicy.ALLOW;
413
+ }
414
+
415
+ return NavigationActionPolicy.ALLOW;
416
+ },
417
+ onConsoleMessage: (controller, consoleMessage) {
418
+ print('Console [\${consoleMessage.messageLevel}]: \${consoleMessage.message}');
419
+ },
420
+ onLoadError: (controller, url, code, message) {
421
+ print('Erro ao carregar \$url: \$message (código: \$code)');
422
+ },
423
+ onLoadHttpError: (controller, url, statusCode, description) {
424
+ print('Erro HTTP ao carregar \$url: \$statusCode - \$description');
425
+ },
426
+ ),
427
+ ),
428
+ ],
429
+ ),
430
+ ),
431
+ );
432
+ }
433
+ }
434
+ `;
435
+ }
436
+ function generateReadme(name, webDir) {
437
+ return `# ${name}
438
+
439
+ Projeto criado com FlutterBridge CLI
440
+
441
+ ## Estrutura do Projeto
442
+
443
+ \`\`\`
444
+ ${name}/
445
+ ├── flutterbridge.config.json # Configuração do FlutterBridge
446
+ ├── flutter_app/ # Projeto Flutter
447
+ │ ├── lib/
448
+ │ │ └── main.dart # App Flutter principal
449
+ │ ├── assets/
450
+ │ │ └── web/ # Arquivos web copiados aqui
451
+ │ └── pubspec.yaml
452
+ └── ${webDir}/ # Build do seu projeto web (React/Vue/Angular)
453
+ \`\`\`
454
+
455
+ ## Comandos Disponíveis
456
+
457
+ ### Sincronizar projeto web com Flutter
458
+ \`\`\`bash
459
+ flutterbridge sync
460
+ \`\`\`
461
+
462
+ ### Executar em emulador/dispositivo
463
+ \`\`\`bash
464
+ flutterbridge run android
465
+ flutterbridge run ios
466
+ \`\`\`
467
+
468
+ ### Build de produção
469
+ \`\`\`bash
470
+ flutterbridge build android --release
471
+ flutterbridge build ios --release
472
+ \`\`\`
473
+
474
+ ### Adicionar plugins Flutter
475
+ \`\`\`bash
476
+ flutterbridge plugin add camera
477
+ flutterbridge plugin add geolocator
478
+ flutterbridge plugin list
479
+ \`\`\`
480
+
481
+ ## Comunicação JavaScript <-> Flutter
482
+
483
+ No seu código JavaScript, você pode se comunicar com o Flutter:
484
+
485
+ \`\`\`javascript
486
+ // Enviar mensagem para Flutter
487
+ window.flutter_inappwebview.callHandler('FlutterBridge', {
488
+ action: 'getData'
489
+ }).then(function(result) {
490
+ console.log('Resposta do Flutter:', result);
491
+ });
492
+ \`\`\`
493
+
494
+ ## Workflow de Desenvolvimento
495
+
496
+ 1. Desenvolva seu app web normalmente (React/Vue/Angular)
497
+ 2. Faça o build: \`npm run build\`
498
+ 3. Sincronize: \`flutterbridge sync\`
499
+ 4. Execute: \`flutterbridge run android\`
500
+
501
+ ## Requisitos
502
+
503
+ - Flutter SDK instalado
504
+ - Node.js 16+
505
+ - Android Studio (para Android)
506
+ - Xcode (para iOS, apenas macOS)
507
+ `;
508
+ }
509
+ // Executa o programa
510
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "flutterbridge",
3
+ "version": "1.0.1",
4
+ "description": "CLI para fazer apps Android via WebView do Flutter",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "flutter",
9
+ "cli",
10
+ "mobile",
11
+ "capacitor",
12
+ "webview"
13
+ ],
14
+ "bin": {
15
+ "flutterbridge": "dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build",
23
+ "dev": "tsx src/index.ts",
24
+ "test": "node dist/index.js --help"
25
+ },
26
+ "dependencies": {
27
+ "chalk": "^5.3.0",
28
+ "commander": "^11.1.0",
29
+ "fs-extra": "^10.1.0",
30
+ "inquirer": "^9.2.12",
31
+ "ora": "^8.0.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/fs-extra": "^11.0.4",
35
+ "@types/inquirer": "^9.0.7",
36
+ "@types/node": "^20.10.0",
37
+ "tsx": "^4.7.0",
38
+ "typescript": "^5.3.3"
39
+ }
40
+ }