@codemoreira/esad 1.3.16 → 1.4.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.
package/README.md CHANGED
@@ -23,11 +23,16 @@ npx esad create-module module-name
23
23
  npx esad create-cdn
24
24
  ```
25
25
 
26
- ### 4. Development & Native Automation
27
- Starts the Rspack server and **automatically patches** native files (Gradle, Entry Points) if necessary.
26
+ ### 4. Development & Cloud-Sync
27
+ Starts the Rspack server locally OR performs a **Dev-Cloud Sync** for remote preview.
28
28
  ```bash
29
- npx esad host dev # Run the Host App
30
- npx esad dev --id module-name --port 9000 # Run a Module
29
+ npx esad dev --id module-name # Builds and pushes bundle to Dev Cloud
30
+ ```
31
+
32
+ ### 5. Host Automation
33
+ Manages the Host App and automatically synchronizes project-wide configurations (Auto-Sync).
34
+ ```bash
35
+ npx esad host android # Run Host on Android
31
36
  ```
32
37
 
33
38
  ### 5. Build for Production
@@ -58,7 +63,7 @@ export default withESAD({
58
63
  ```
59
64
 
60
65
  ### ⚡ Global State SDK (`@codemoreira/esad/client`)
61
- Share state across the Host and all Remote Modules reactively:
66
+ Share state across the Host and all Remote Modules reactively via a **Universal Singleton**:
62
67
  ```javascript
63
68
  import { useESADState } from '@codemoreira/esad/client';
64
69
 
@@ -67,6 +72,15 @@ const [user, setUser] = useESADState('user');
67
72
 
68
73
  ---
69
74
 
75
+ ## ⚙️ Configuration (`esad.config.json`)
76
+
77
+ ESAD supports a "Zero-Config" but powerful orchestration file at the root:
78
+ - `projectName`: Your SuperApp identifier.
79
+ - `devModeFor`: Array of **shorthand** module names (e.g. `["recebimento"]`) to load from the Dev Cloud instead of Production.
80
+ - **Auto-Sync**: CLI commands (`dev`, `build`, `deploy`) automatically propagate this config to the Host App.
81
+
82
+ ---
83
+
70
84
  ## 🏠 Template Features (Host & Module)
71
85
 
72
86
  ESAD provides high-quality, boilerplate-free templates:
package/bin/esad.js CHANGED
@@ -27,8 +27,8 @@ program
27
27
 
28
28
  // --- COMMAND: esad create-cdn ---
29
29
  program
30
- .command('create-cdn [cdn-name]')
31
- .description('Scaffold the CDN / Registry backend')
30
+ .command('create-cdn')
31
+ .description('Scaffold the CDN backend (always named {{projectName}}-cdn)')
32
32
  .action(async (name) => {
33
33
  await createCdnCommand(name);
34
34
  process.exit(0);
@@ -37,7 +37,7 @@ program
37
37
  // --- COMMAND: esad host ---
38
38
  program
39
39
  .command('host <subcommand>')
40
- .description('Manage the Host App (dev, android, ios)')
40
+ .description('Manage the Host App (Auto-Syncs config, runs android/ios)')
41
41
  .action(async (sub) => {
42
42
  await hostCommand(sub);
43
43
  process.exit(0);
@@ -78,9 +78,9 @@ program
78
78
  // --- COMMAND: esad dev ---
79
79
  program
80
80
  .command('dev')
81
- .option('-i, --id <moduleId>', 'The Module ID to run in dev mode')
82
- .option('-p, --port <port>', 'The port to run the dev server on', '8081')
83
- .description('Starts the dev server and updates the external registry to bypass CDN')
81
+ .option('-i, --id <moduleId>', 'The Module ID to sync to Dev-Cloud')
82
+ .option('-p, --platform <platform>', 'Platform (android, ios)', 'android')
83
+ .description('Build and Push the module bundle to the Dev-Cloud for global previewing')
84
84
  .action(async (options) => {
85
85
  await devCommand(options);
86
86
  // Note: dev command has its own shutdown logic with SIGINT/SIGTERM
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemoreira/esad",
3
- "version": "1.3.16",
3
+ "version": "1.4.1",
4
4
  "description": "Easy Super App Development - Zero-Config CLI and DevTools for React Native Module Federation",
5
5
  "main": "src/plugin/index.js",
6
6
  "types": "./src/plugin/index.d.ts",
@@ -2,7 +2,7 @@ const { runProcess } = require('../utils/process');
2
2
  const path = require('path');
3
3
  const fs = require('fs-extra');
4
4
  const chalk = require('chalk');
5
- const { getWorkspaceConfig } = require('../utils/config');
5
+ const { getWorkspaceConfig, syncHostConfig } = require('../utils/config');
6
6
  const { resolveProjectDir, listAvailableModules } = require('../utils/resolution');
7
7
 
8
8
  module.exports = async (options) => {
@@ -15,6 +15,8 @@ module.exports = async (options) => {
15
15
  process.exit(1);
16
16
  }
17
17
 
18
+ syncHostConfig(configObj);
19
+
18
20
  const { projectName } = configObj.data;
19
21
 
20
22
  if (options.id) {
@@ -10,7 +10,8 @@ module.exports = async (cdnName) => {
10
10
  return;
11
11
  }
12
12
 
13
- const finalCdnName = cdnName || `${configObj.data.projectName}-cdn`;
13
+ const { projectName } = configObj.data;
14
+ const finalCdnName = `${projectName}-cdn`;
14
15
  const cdnPath = path.join(process.cwd(), finalCdnName);
15
16
 
16
17
  if (fs.existsSync(cdnPath)) {
@@ -2,7 +2,7 @@ const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const AdmZip = require('adm-zip');
4
4
  const chalk = require('chalk');
5
- const { getWorkspaceConfig } = require('../utils/config');
5
+ const { getWorkspaceConfig, syncHostConfig } = require('../utils/config');
6
6
  const { resolveModuleMetadata, listAvailableModules } = require('../utils/resolution');
7
7
 
8
8
  module.exports = async (options) => {
@@ -19,6 +19,8 @@ module.exports = async (options) => {
19
19
  const workspaceRoot = path.dirname(configObj.path);
20
20
  const { projectName } = configObj.data;
21
21
 
22
+ syncHostConfig(configObj);
23
+
22
24
  let moduleId = options.id;
23
25
 
24
26
  if (moduleId) {
@@ -1,10 +1,11 @@
1
1
  const { spawn } = require('cross-spawn');
2
- const { getWorkspaceConfig } = require('../utils/config');
2
+ const { getWorkspaceConfig, syncHostConfig } = require('../utils/config');
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const chalk = require('chalk');
6
- const { prepareNative } = require('../utils/scaffold');
7
- const { resolveProjectDir, listAvailableModules } = require('../utils/resolution');
6
+ const { resolveModuleMetadata, listAvailableModules } = require('../utils/resolution');
7
+ const { runProcess } = require('../utils/process');
8
+ const AdmZip = require('adm-zip');
8
9
 
9
10
  module.exports = async (options) => {
10
11
  let cwd = process.cwd();
@@ -20,6 +21,9 @@ module.exports = async (options) => {
20
21
  const workspaceRoot = path.dirname(configObj.path);
21
22
  const { projectName } = configObj.data;
22
23
 
24
+ // Synchronize Host Config
25
+ syncHostConfig(configObj);
26
+
23
27
  if (options.id) {
24
28
  const targetDir = resolveProjectDir(options.id, configObj);
25
29
  if (!targetDir) {
@@ -40,50 +44,80 @@ module.exports = async (options) => {
40
44
  }
41
45
  }
42
46
 
47
+ if (!fs.existsSync(pkgPath)) {
48
+ console.error(chalk.red(`❌ Error: package.json not found in ${cwd}.`));
49
+ process.exit(1);
50
+ }
51
+
43
52
  const pkg = fs.readJsonSync(pkgPath);
44
- const moduleId = options.id || pkg.name;
45
- const port = options.port || '8081';
53
+ moduleId = moduleId || pkg.name;
54
+ const platform = options.platform || 'android';
46
55
 
47
- // Determine if it's a Host or Module
48
- const isHost = pkg.name.endsWith('-host') || pkg.dependencies?.['@callstack/repack'];
56
+ console.log(chalk.cyan(`\n☁️ Starting ESAD Dev-Push for ${moduleId} (${platform})\n`));
49
57
 
50
- // 1. Initial Checks & Automated Native Preparation
51
- await prepareNative(cwd, 'all');
52
-
53
- if (isHost && !options.id) {
54
- console.log(`\n🚀 Starting Host App Dev Server (Re.Pack/Rspack)...\n`);
55
- await spawn('npx', ['react-native', 'webpack-start'], { cwd, stdio: 'inherit', shell: true });
56
- return;
58
+ const config = configObj ? configObj.data : null;
59
+ const devUrlBase = config?.deployEndpoint || config?.devEndpoint;
60
+
61
+ if (!devUrlBase) {
62
+ console.error(chalk.red(`❌ Error: 'deployEndpoint' not configured in esad.config.json.`));
63
+ process.exit(1);
57
64
  }
58
65
 
59
- console.log(`\n⚡ Starting ESAD Dev Server for ${moduleId} on port ${port}...\n`);
60
-
61
- const config = configObj ? configObj.data : null;
62
- let devApiUrl = config?.devModeEndpoint ? config.devModeEndpoint.replace('{{moduleId}}', moduleId) : null;
66
+ const devUploadUrl = devUrlBase.replace('{{moduleId}}', moduleId) + '/dev';
67
+ console.log(`📡 Dev-Cloud Endpoint: ${devUploadUrl}`);
63
68
 
64
- const setDevMode = async (isActive) => {
65
- if (!devApiUrl) return;
66
- try {
67
- const body = {
68
- is_dev_mode: isActive,
69
- ...(isActive && { dev_url: `http://localhost:${port}/index.bundle` })
70
- };
71
- const res = await fetch(devApiUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
72
- if (res.ok) console.log(`📡 Registry Sync: Dev Override is ${isActive ? 'ON' : 'OFF'} for module ${moduleId}`);
73
- } catch(e) { /* ignore */ }
74
- };
69
+ // 1. Build
70
+ console.log(`\n🏗️ Step 1/3: Building bundle...`);
71
+ const bundleOutput = path.join(cwd, 'build', platform, 'index.bundle');
72
+ await fs.ensureDir(path.dirname(bundleOutput));
73
+
74
+ try {
75
+ await runProcess('npx', [
76
+ 'react-native',
77
+ 'webpack-bundle',
78
+ '--platform', platform,
79
+ '--dev', 'false',
80
+ '--bundle-output', bundleOutput,
81
+ '--assets-dest', path.dirname(bundleOutput)
82
+ ], cwd);
83
+ } catch (err) {
84
+ console.error(chalk.red(`❌ Build failed.`));
85
+ process.exit(1);
86
+ }
75
87
 
76
- await setDevMode(true);
88
+ // 2. Zip
89
+ console.log(`\n🗜️ Step 2/3: Zipping assets...`);
90
+ const zip = new AdmZip();
91
+ const buildDir = path.join(cwd, 'build');
92
+ zip.addLocalFolder(buildDir);
93
+ const zipPath = path.join(cwd, `dev-bundle-${moduleId}.zip`);
94
+ zip.writeZip(zipPath);
77
95
 
78
- const proc = spawn('npx', ['react-native', 'webpack-start', '--port', port], { cwd, stdio: 'inherit', shell: true });
96
+ // 3. Upload
97
+ console.log(`\n🚀 Step 3/3: Pushing to Dev-Cloud...`);
98
+ try {
99
+ const FormData = require('form-data');
100
+ const form = new FormData();
101
+ form.append('bundle', fs.createReadStream(zipPath));
79
102
 
80
- const shutdown = async () => {
81
- console.log(`\n🛑 Stopping ESAD Dev Server and reverting registry status...`);
82
- await setDevMode(false);
83
- proc.kill();
84
- process.exit(0);
85
- };
103
+ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
104
+ const response = await fetch(devUploadUrl, {
105
+ method: 'POST',
106
+ body: form,
107
+ headers: form.getHeaders(),
108
+ });
86
109
 
87
- process.on('SIGINT', shutdown);
88
- process.on('SIGTERM', shutdown);
110
+ if (response.ok) {
111
+ console.log(chalk.green(`\n✅ Successfully synced ${moduleId} to Dev-Cloud!`));
112
+ console.log(`📱 Host app configured with 'devModeFor: ["${options.id || moduleId}"]' will now load this version.\n`);
113
+ } else {
114
+ const errorText = await response.text();
115
+ console.error(chalk.red(`❌ Cloud Sync failed: ${response.status} ${response.statusText}`));
116
+ console.error(errorText);
117
+ }
118
+ } catch (err) {
119
+ console.error(chalk.red(`❌ Error during sync: ${err.message}`));
120
+ } finally {
121
+ if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
122
+ }
89
123
  };
@@ -2,10 +2,36 @@ const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
 
4
4
  const getWorkspaceConfig = () => {
5
- let configPath = path.join(process.cwd(), 'esad.config.json');
6
- if (!fs.existsSync(configPath)) configPath = path.join(process.cwd(), '../esad.config.json');
5
+ let cwd = process.cwd();
6
+ let configPath = path.join(cwd, 'esad.config.json');
7
+
8
+ // Search up to 2 levels (root or project folder)
9
+ if (!fs.existsSync(configPath)) configPath = path.join(cwd, '..', 'esad.config.json');
10
+ if (!fs.existsSync(configPath)) configPath = path.join(cwd, '..', '..', 'esad.config.json');
11
+
7
12
  if (!fs.existsSync(configPath)) return null;
8
13
  return { path: configPath, data: fs.readJsonSync(configPath) };
9
14
  };
10
15
 
11
- module.exports = { getWorkspaceConfig };
16
+ const syncHostConfig = (configObj) => {
17
+ if (!configObj) return;
18
+ const { projectName } = configObj.data;
19
+ const workspaceRoot = path.dirname(configObj.path);
20
+ const hostDir = path.join(workspaceRoot, `${projectName}-host`);
21
+
22
+ if (fs.existsSync(hostDir)) {
23
+ const hostConfigPath = path.join(hostDir, 'esad.config.json');
24
+
25
+ // Only pass client-safe fields
26
+ const clientConfig = {
27
+ projectName: configObj.data.projectName,
28
+ registryUrl: configObj.data.registryUrl,
29
+ devModeFor: configObj.data.devModeFor || []
30
+ };
31
+
32
+ fs.writeJsonSync(hostConfigPath, clientConfig, { spaces: 2 });
33
+ console.log(`\n🔄 Sync: Config propagated to ${path.relative(workspaceRoot, hostConfigPath)}`);
34
+ }
35
+ };
36
+
37
+ module.exports = { getWorkspaceConfig, syncHostConfig };
@@ -5,34 +5,47 @@ import { useState, useEffect } from 'react';
5
5
  * This class runs as a true Singleton across the Host and all Federated Modules,
6
6
  * allowing instant variable sharing without tight coupling.
7
7
  */
8
- class ESADEventEmitter {
9
- constructor() {
10
- this.state = {};
11
- this.listeners = {};
12
- }
8
+ // Unique key to store the global state in the environment (shared across sessions)
9
+ const GLOBAL_STORE_KEY = '__ESAD_GLOBAL_STATE__';
10
+
11
+ // Initialize the global store if it doesn't already exist.
12
+ // This ensures that even if different chunks/modules have their own copy
13
+ // of this JS file, they all point to the same memory object in globalThis.
14
+ if (!globalThis[GLOBAL_STORE_KEY]) {
15
+ globalThis[GLOBAL_STORE_KEY] = {
16
+ state: {},
17
+ listeners: {}
18
+ };
19
+ }
20
+
21
+ const GlobalStore = globalThis[GLOBAL_STORE_KEY];
13
22
 
23
+ class ESADEventEmitter {
14
24
  set(key, value) {
15
- this.state[key] = value;
16
- if (this.listeners[key]) {
17
- this.listeners[key].forEach(callback => callback(value));
25
+ GlobalStore.state[key] = value;
26
+ if (GlobalStore.listeners[key]) {
27
+ GlobalStore.listeners[key].forEach(callback => callback(value));
18
28
  }
19
29
  }
20
30
 
21
31
  get(key) {
22
- return this.state[key];
32
+ return GlobalStore.state[key];
23
33
  }
24
34
 
25
35
  subscribe(key, callback) {
26
- if (!this.listeners[key]) this.listeners[key] = [];
27
- this.listeners[key].push(callback);
36
+ if (!GlobalStore.listeners[key]) {
37
+ GlobalStore.listeners[key] = [];
38
+ }
39
+ GlobalStore.listeners[key].push(callback);
40
+
41
+ // Return unsubscribe function
28
42
  return () => {
29
- this.listeners[key] = this.listeners[key].filter(cb => cb !== callback);
43
+ GlobalStore.listeners[key] = GlobalStore.listeners[key].filter(cb => cb !== callback);
30
44
  };
31
45
  }
32
46
  }
33
47
 
34
- // Because this package is marked as a ModuleFederation Singleton,
35
- // this instance will be shared identically across all chunks!
48
+ // Global instance (acts as a proxy to the globalStore)
36
49
  const ESADState = new ESADEventEmitter();
37
50
 
38
51
  /**
@@ -24,6 +24,7 @@ function withESAD(env, options) {
24
24
  const pkgPath = path.resolve(dirname, 'package.json');
25
25
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
26
26
  const id = options.id.replace(/-/g, '_');
27
+ const clientPath = path.resolve(__dirname, '..', 'client', 'index.js');
27
28
 
28
29
  console.log(`[ESAD] Applying Mega-Zero-Config profile for ${options.type.toUpperCase()} (${platform}): ${id}`);
29
30
 
@@ -96,8 +97,11 @@ function withESAD(env, options) {
96
97
  'react/jsx-runtime': { singleton: true, eager: true, requiredVersion: pkg.dependencies.react },
97
98
  'react-native': { singleton: true, eager: true, requiredVersion: pkg.dependencies['react-native'] },
98
99
  'react-native-safe-area-context': { singleton: true, eager: true, requiredVersion: pkg.dependencies['react-native-safe-area-context'] },
99
- '@codemoreira/esad/client': { singleton: true, eager: true },
100
- '@codemoreira/esad': { singleton: true, eager: true },
100
+ '@codemoreira/esad/client': {
101
+ singleton: true,
102
+ eager: options.type === 'host', // Only eager in host to ensure it's available
103
+ import: clientPath
104
+ },
101
105
  ...(options.shared || {})
102
106
  }
103
107
  })