@codemoreira/esad 2.0.1-2 → 2.0.1-4

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/bin/esad.js CHANGED
@@ -29,7 +29,7 @@ program
29
29
  .command('dev [id]') // [id] as alias to -i for better UX
30
30
  .option('-i, --id <moduleId>', 'The Module ID to run in dev mode')
31
31
  .option('-p, --port <port>', 'The port to run the dev server on', '8081')
32
- .description('Starts the dev server and updates the local mapping')
32
+ .description('Starts the development environment for a module or the host application')
33
33
  .action(async (id, options) => {
34
34
  const opts = { ...options, id: id || options.id };
35
35
  await require('../src/cli/commands/dev')(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemoreira/esad",
3
- "version": "2.0.1-2",
3
+ "version": "2.0.1-4",
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",
@@ -1,72 +1,172 @@
1
- const { runProcess } = require('../utils/process');
2
- const { getWorkspaceConfig } = require('../utils/config');
3
- const fs = require('fs-extra');
4
- const path = require('path');
5
- const chalk = require('chalk');
6
- const { prepareNative } = require('../utils/scaffold');
7
- const { resolveProjectDir } = require('../utils/resolution');
8
-
9
- module.exports = async (options) => {
10
- const configObj = getWorkspaceConfig();
11
- if (!configObj) {
12
- console.error(chalk.red(`❌ Error: esad.config.js not found. Run this from your project root.`));
13
- process.exit(1);
14
- }
15
-
16
- const config = await configObj.load();
17
- const workspaceRoot = configObj.root;
18
- const projectName = config.default?.projectName || config.projectName;
19
-
20
- let cwd = process.cwd();
21
- let selectedModuleId = options.id;
22
-
23
- if (selectedModuleId) {
24
- const targetDir = resolveProjectDir(selectedModuleId, configObj);
25
- if (!targetDir) {
26
- console.error(chalk.red(`❌ Error: Module not found: ${selectedModuleId}`));
27
- process.exit(1);
28
- }
29
- cwd = targetDir;
30
- }
31
-
32
- const pkgPath = path.join(cwd, 'package.json');
33
- const pkg = fs.readJsonSync(pkgPath);
34
- const moduleId = selectedModuleId || pkg.name;
35
- const port = options.port || '8081';
36
-
37
- // Determine if it's a Host or Module
38
- const isHost = pkg.name.endsWith('-host') || pkg.dependencies?.['@callstack/repack'];
39
-
40
- await prepareNative(cwd, 'all');
41
-
42
- if (isHost && !selectedModuleId) {
43
- console.log(`\n🚀 Starting ${chalk.green('Host App')} Dev Server (Re.Pack/Rspack)...\n`);
44
- await runProcess('npx', ['react-native', 'webpack-start'], { cwd });
45
- return;
46
- }
47
-
48
- const { updateDevMode, removeDevMode, syncContextDownwards } = require('../utils/transformer');
49
-
50
- console.log(`\n⚡ Starting ESAD Dev Server for ${chalk.cyan(moduleId)} on port ${port}...\n`);
51
-
52
- // Automate devMode update in esad.config.js
53
- const localBundleUrl = `http://localhost:${port}/index.bundle`;
54
- updateDevMode(configObj.path, moduleId, localBundleUrl);
55
- syncContextDownwards(configObj);
56
- console.log(chalk.gray(`📡 Mode: Module Dev. Host configured to load ${moduleId} from ${localBundleUrl}`));
57
-
58
- const proc = runProcess('npx', ['react-native', 'webpack-start', '--port', port], { cwd });
59
-
60
- const shutdown = async () => {
61
- console.log(`\n🛑 Stopping ESAD Dev Server and reverting config...`);
62
- removeDevMode(configObj.path, moduleId);
63
- syncContextDownwards(configObj);
64
- if (proc.kill) proc.kill();
65
- process.exit(0);
66
- };
67
-
68
-
69
- process.on('SIGINT', shutdown);
70
- process.on('SIGTERM', shutdown);
71
- };
72
-
1
+ const { runProcess } = require('../utils/process');
2
+ const { getWorkspaceConfig } = require('../utils/config');
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const http = require('http');
7
+ const readline = require('readline');
8
+ const { spawn } = require('cross-spawn');
9
+ const { prepareNative } = require('../utils/scaffold');
10
+ const { resolveProjectDir } = require('../utils/resolution');
11
+
12
+ /**
13
+ * Check if a port is in use
14
+ */
15
+ const isPortInUse = (port) => new Promise((resolve) => {
16
+ const req = http.get(`http://localhost:${port}`, (res) => {
17
+ resolve(true);
18
+ });
19
+ req.on('error', () => {
20
+ resolve(false);
21
+ });
22
+ req.end();
23
+ });
24
+
25
+ const runHostDevFlow = async (cwd) => {
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout
29
+ });
30
+ const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
31
+
32
+ console.log(`\n${chalk.bold('ESAD Host Dev Manager')}`);
33
+ console.log(chalk.dim(`---------------------`));
34
+ console.log(`[a] Run on Android`);
35
+ console.log(`[i] Run on iOS`);
36
+ console.log(`[b] Bundler Only`);
37
+ console.log(`[c] Cancel`);
38
+
39
+ const choice = (await askQuestion(`\nSelect platform: `)).toLowerCase();
40
+
41
+ if (choice === 'c') {
42
+ console.log(`\n❌ Cancelled.`);
43
+ rl.close();
44
+ return;
45
+ }
46
+
47
+ const portBusy = await isPortInUse(8081);
48
+ let shouldStartBundler = true;
49
+
50
+ if (portBusy) {
51
+ console.log(`\n⚠️ Warning: Port 8081 is already in use.`);
52
+ console.log(`💡 Skipping Bundler startup. Proceeding with Native Build.\n`);
53
+ shouldStartBundler = false;
54
+ }
55
+
56
+ if (shouldStartBundler && choice !== 'c') {
57
+ console.log(`\n🛠️ Starting Rspack Bundler...`);
58
+ if (process.platform === 'win32') {
59
+ spawn('cmd', ['/c', 'start', '/D', cwd, 'npx.cmd', 'react-native', 'webpack-start'], {
60
+ detached: true,
61
+ stdio: 'ignore',
62
+ shell: true
63
+ }).unref();
64
+ } else {
65
+ spawn('npx', ['react-native', 'webpack-start'], {
66
+ cwd,
67
+ detached: true,
68
+ stdio: 'inherit',
69
+ shell: true
70
+ }).unref();
71
+ }
72
+
73
+ console.log(`⏳ Waiting for Rspack Bundler on port 8081...`);
74
+ const waitForBundler = async () => {
75
+ for (let i = 0; i < 30; i++) {
76
+ if (await isPortInUse(8081)) return true;
77
+ await new Promise(r => setTimeout(r, 2000));
78
+ }
79
+ return false;
80
+ };
81
+
82
+ if (!(await waitForBundler())) {
83
+ console.error(`\n❌ Timeout: Bundler did not respond.`);
84
+ rl.close();
85
+ return;
86
+ }
87
+ console.log(`✅ Bundler ready!`);
88
+ }
89
+
90
+ if (choice === 'a') {
91
+ console.log(`🤖 Launching Android...`);
92
+ await runProcess('react-native', ['run-android', '--no-packager'], cwd);
93
+ } else if (choice === 'i') {
94
+ console.log(`🍎 Launching iOS...`);
95
+ await runProcess('react-native', ['run-ios', '--no-packager'], cwd);
96
+ }
97
+
98
+ rl.close();
99
+ };
100
+
101
+ module.exports = async (options) => {
102
+ const configObj = getWorkspaceConfig();
103
+ if (!configObj) {
104
+ console.error(chalk.red(`❌ Error: esad.config.js not found. Run this from your project root.`));
105
+ process.exit(1);
106
+ }
107
+
108
+ const config = configObj.data;
109
+ const workspaceRoot = configObj.root;
110
+ const projectName = config.projectName;
111
+
112
+ let cwd = process.cwd();
113
+ let selectedModuleId = options.id;
114
+
115
+ // 1. Module Dev Flow
116
+ if (selectedModuleId) {
117
+ const targetDir = resolveProjectDir(selectedModuleId, configObj);
118
+ if (!targetDir) {
119
+ console.error(chalk.red(`❌ Error: Module not found: ${selectedModuleId}`));
120
+ process.exit(1);
121
+ }
122
+ cwd = targetDir;
123
+
124
+ const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
125
+ const moduleId = selectedModuleId || pkg.name;
126
+ const port = options.port || '8081';
127
+
128
+ await prepareNative(cwd, 'all');
129
+
130
+ const { updateDevMode, removeDevMode, syncContextDownwards } = require('../utils/transformer');
131
+
132
+ console.log(`\n⚡ Starting ESAD Dev Server for ${chalk.cyan(moduleId)} on port ${port}...\n`);
133
+ const localBundleUrl = `http://localhost:${port}/index.bundle`;
134
+ updateDevMode(configObj.path, moduleId, localBundleUrl);
135
+ syncContextDownwards(configObj);
136
+
137
+ const proc = runProcess('npx', ['react-native', 'webpack-start', '--port', port], { cwd });
138
+
139
+ const shutdown = async () => {
140
+ console.log(`\n🛑 Stopping ESAD Dev Server and reverting config...`);
141
+ removeDevMode(configObj.path, moduleId);
142
+ syncContextDownwards(configObj);
143
+ if (proc.kill) proc.kill();
144
+ process.exit(0);
145
+ };
146
+
147
+ process.on('SIGINT', shutdown);
148
+ process.on('SIGTERM', shutdown);
149
+ return;
150
+ }
151
+
152
+ // 2. Host Dev Flow (Auto-detection)
153
+ const pkgPath = path.join(cwd, 'package.json');
154
+ let isHost = fs.existsSync(pkgPath) && fs.readJsonSync(pkgPath).name.endsWith('-host');
155
+
156
+ if (!isHost) {
157
+ const hostDir = path.join(workspaceRoot, `${projectName}-host`);
158
+ if (fs.existsSync(hostDir)) {
159
+ cwd = hostDir;
160
+ isHost = true;
161
+ console.log(`📂 Auto-detected Host App: ${chalk.dim(path.relative(process.cwd(), hostDir))}`);
162
+ }
163
+ }
164
+
165
+ if (isHost) {
166
+ await prepareNative(cwd, 'all');
167
+ await runHostDevFlow(cwd);
168
+ } else {
169
+ console.error(chalk.red(`❌ Error: Could not detect Host or Module context.`));
170
+ console.log(`👉 Try: esad dev <module-id>`);
171
+ }
172
+ };
@@ -1,152 +1,42 @@
1
- const { runProcess } = require('../utils/process');
2
- const path = require('path');
3
- const fs = require('fs-extra');
4
- const { spawn } = require('cross-spawn');
5
- const http = require('http');
6
- const readline = require('readline');
7
- const { getWorkspaceConfig } = require('../utils/config');
8
- const { prepareNative } = require('../utils/scaffold');
9
-
10
- const rl = readline.createInterface({
11
- input: process.stdin,
12
- output: process.stdout
13
- });
14
-
15
- const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
16
-
17
- /**
18
- * Check if a port is in use
19
- */
20
- const isPortInUse = (port) => new Promise((resolve) => {
21
- const req = http.get(`http://localhost:${port}`, (res) => {
22
- resolve(true); // Responded, so it's in use
23
- });
24
- req.on('error', () => {
25
- resolve(false); // Refused, so it's free
26
- });
27
- req.end();
28
- });
29
-
30
- module.exports = async (subcommand) => {
31
- let cwd = process.cwd();
32
- let pkgPath = path.join(cwd, 'package.json');
33
-
34
- // Try to find workspace config to resolve host path from root
35
- const configObj = getWorkspaceConfig();
36
- if (configObj) {
37
- const workspaceRoot = path.dirname(configObj.path);
38
- const { projectName } = configObj.data;
39
- const hostDir = path.join(workspaceRoot, `${projectName}-host`);
40
-
41
- if (fs.existsSync(hostDir)) {
42
- cwd = hostDir;
43
- pkgPath = path.join(cwd, 'package.json');
44
- console.log(`📂 Auto-detected Host App folder: ${path.relative(process.cwd(), hostDir)}`);
45
- }
46
- }
47
-
48
- if (!fs.existsSync(pkgPath)) {
49
- console.error(`❌ Error: Call this command from inside the Host App or the Workspace Root.`);
50
- return;
51
- }
52
-
53
- const pkg = fs.readJsonSync(pkgPath);
54
-
55
- // 1. Initial Checks & Automated Native Preparation
56
- if (subcommand === 'dev' || subcommand === 'start') {
57
- await prepareNative(cwd, 'all');
58
-
59
- // 3. Platform Selection
60
- console.log(`\nESAD Host Dev Manager`);
61
- console.log(`---------------------`);
62
- console.log(`[a] Run on Android`);
63
- console.log(`[i] Run on iOS`);
64
- console.log(`[b] Bundler Only`);
65
- console.log(`[c] Cancel`);
66
-
67
- const choice = (await askQuestion(`\nSelect platform: `)).toLowerCase();
68
-
69
- if (choice === 'c') {
70
- console.log(`\n❌ Cancelled.`);
71
- rl.close();
72
- return;
73
- }
74
-
75
- // 4. Check for Port 8081
76
- const portBusy = await isPortInUse(8081);
77
- let shouldStartBundler = true;
78
-
79
- if (portBusy) {
80
- console.log(`\n⚠️ Warning: Port 8081 is already in use by another process.`);
81
- console.log(`💡 Skipping Bundler startup - assuming it's already running. Proceeding with Native Build only.\n`);
82
- shouldStartBundler = false;
83
- }
84
-
85
- // 4. Start Bundler in a New Window (if needed)
86
- if (shouldStartBundler && choice !== 'c') {
87
- console.log(`\n🛠️ Starting Rspack Bundler in a new window...`);
88
- if (process.platform === 'win32') {
89
- const npxCmd = 'npx.cmd';
90
- spawn('cmd', ['/c', 'start', '/D', cwd, npxCmd, 'react-native', 'webpack-start'], {
91
- detached: true,
92
- stdio: 'ignore',
93
- shell: true
94
- }).unref();
95
- } else {
96
- spawn('npx', ['react-native', 'webpack-start'], {
97
- cwd,
98
- detached: true,
99
- stdio: 'inherit',
100
- shell: true
101
- }).unref();
102
- }
103
-
104
- // 5. Wait for Bundler (Port 8081)
105
- console.log(`⏳ Waiting for Rspack Bundler to initialize on port 8081...`);
106
- const waitForBundler = async () => {
107
- for (let i = 0; i < 30; i++) {
108
- if (await isPortInUse(8081)) return true;
109
- await new Promise(r => setTimeout(r, 2000));
110
- }
111
- return false;
112
- };
113
-
114
- const isReady = await waitForBundler();
115
- if (!isReady) {
116
- console.error(`\n❌ Timeout: Bundler did not respond after 60 seconds.`);
117
- rl.close();
118
- return;
119
- }
120
- console.log(`✅ Bundler stable and ready to use!`);
121
- }
122
-
123
- // 6. Launch Native App
124
- if (choice === 'a') {
125
- console.log(`🤖 Compiling and launching on Android...`);
126
- await runProcess('react-native', ['run-android', '--no-packager'], cwd);
127
- } else if (choice === 'i') {
128
- console.log(`🍎 Compiling and launching on iOS...`);
129
- await runProcess('react-native', ['run-ios', '--no-packager'], cwd);
130
- } else if (choice === 'b') {
131
- if (portBusy) {
132
- console.log(`✨ Bundler is already active. You can launch manual native runs.`);
133
- } else {
134
- console.log(`✨ Bundler is running. You can open the app manually.`);
135
- }
136
- }
137
-
138
- rl.close();
139
- } else {
140
- // Other subcommands (android, ios directly)
141
- try {
142
- if (subcommand === 'android') {
143
- await runProcess('react-native', ['run-android', '--no-packager'], cwd);
144
- } else if (subcommand === 'ios') {
145
- await runProcess('react-native', ['run-ios', '--no-packager'], cwd);
146
- }
147
- } catch (err) {
148
- console.error(`❌ Error running host command: ${err.message}`);
149
- }
150
- rl.close();
151
- }
152
- };
1
+ const { runProcess } = require('../utils/process');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const { getWorkspaceConfig } = require('../utils/config');
5
+
6
+ module.exports = async (subcommand) => {
7
+ let cwd = process.cwd();
8
+ let pkgPath = path.join(cwd, 'package.json');
9
+
10
+ const configObj = getWorkspaceConfig();
11
+ if (configObj) {
12
+ const workspaceRoot = configObj.root;
13
+ const { projectName } = configObj.data;
14
+ const hostDir = path.join(workspaceRoot, `${projectName}-host`);
15
+
16
+ if (fs.existsSync(hostDir)) {
17
+ cwd = hostDir;
18
+ pkgPath = path.join(cwd, 'package.json');
19
+ }
20
+ }
21
+
22
+ if (!fs.existsSync(pkgPath)) {
23
+ console.error(`❌ Error: Call this command from inside the Host App or the Workspace Root.`);
24
+ return;
25
+ }
26
+
27
+ // Handle direct platform commands
28
+ try {
29
+ if (subcommand === 'android') {
30
+ console.log(`🤖 Compiling and launching on Android...`);
31
+ await runProcess('react-native', ['run-android', '--no-packager'], cwd);
32
+ } else if (subcommand === 'ios') {
33
+ console.log(`🍎 Compiling and launching on iOS...`);
34
+ await runProcess('react-native', ['run-ios', '--no-packager'], cwd);
35
+ } else {
36
+ console.log(`\n💡 Tip: Use 'esad dev' for the interactive dev manager.`);
37
+ console.log(` Or use 'esad host android' / 'esad host ios' for direct runs.`);
38
+ }
39
+ } catch (err) {
40
+ console.error(`❌ Error running host command: ${err.message}`);
41
+ }
42
+ };
@@ -63,13 +63,23 @@ export default {
63
63
  await renameProject(hostDir, hostName);
64
64
 
65
65
  // Inject local context mock immediately to avoid crashes on fresh boot
66
- fs.writeJsonSync(path.join(hostDir, '.esad.context.json'), { projectName, devMode: {} }, { spaces: 2 });
66
+ const context = { projectName, devMode: {} };
67
+ fs.writeJsonSync(path.join(hostDir, '.esad.context.json'), context, { spaces: 2 });
67
68
 
68
- console.log(`\n📦 Installing dependencies into host...`);
69
+ // Stabilize filesystem before heavy operations
70
+ await new Promise(res => setTimeout(res, 500));
71
+
72
+ console.log(`\n📦 Installing dependencies into host (this may take a minute)...`);
69
73
  await runProcess('npm', ['install'], { cwd: hostDir });
70
- console.log(`\n🎉 ESAD Workspace Initialized!`);
71
- console.log(`-> cd ${projectName}/${hostName}\n-> esad host dev (to start Host)`);
74
+
75
+ console.log(chalk.green(`\n🎉 ESAD Workspace Initialized successfully!`));
76
+ console.log(chalk.cyan(`\n👉 Next steps:`));
77
+ console.log(` 1. cd ${projectName}/${hostName}`);
78
+ console.log(` 2. esad host dev (to start Host)`);
79
+ console.log(` 3. esad dev (in a module folder to federate)\n`);
72
80
  } catch (err) {
73
- console.error(`❌ Failed to init Host:`, err.message);
81
+ console.error(chalk.red(`\n❌ Failed to initialize workspace:`));
82
+ console.error(chalk.yellow(` ${err.message}`));
83
+ console.log(chalk.dim(`\n Check npm logs if it was an installation error.`));
74
84
  }
75
85
  };
@@ -20,15 +20,14 @@ const getWorkspaceConfig = () => {
20
20
 
21
21
  try {
22
22
  const jiti = createJiti(__filename);
23
- const configModule = jiti(configPath);
24
- // jiti.import returns a promise for async imports if needed, but for esad.config.js
25
- // we expect a sync structure or we resolve it.
26
- // However, jiti v2 import is async.
27
-
23
+ let configData = jiti(configPath);
24
+ if (configData.default) configData = configData.default;
25
+
28
26
  return {
29
27
  path: configPath,
30
28
  root: path.dirname(configPath),
31
- load: () => configModule
29
+ data: configData,
30
+ load: () => configData
32
31
  };
33
32
  } catch (err) {
34
33
  console.error(`❌ Failed to load esad.config.js: ${err.message}`);
@@ -3,7 +3,8 @@ const nativeSpawn = require('child_process').spawn;
3
3
  const path = require('path');
4
4
  const fs = require('fs-extra');
5
5
 
6
- const runProcess = (cmd, args, cwd = process.cwd()) => {
6
+ const runProcess = (cmd, args, options = process.cwd()) => {
7
+ const cwd = typeof options === 'string' ? options : (options.cwd || process.cwd());
7
8
  let childRef;
8
9
  const promise = new Promise((resolve, reject) => {
9
10
  let finished = false;
@@ -10,20 +10,19 @@ const updateDevMode = (configPath, moduleId, url) => {
10
10
 
11
11
  // 1. Ensure devMode object exists
12
12
  if (!content.includes('devMode:')) {
13
- // Inject devMode before the last closing brace (naive but effective for clean configs)
14
- content = content.replace(/}([^}]*)$/, ` devMode: {},\n}$1`);
13
+ // Inject devMode after the opening brace of export default
14
+ content = content.replace(/(export default\s*\{)/, `$1\n devMode: {},\n`);
15
15
  }
16
16
 
17
17
  // 2. Add or update the module entry
18
18
  const entryRegex = new RegExp(`(['"]${moduleId}['"]|${moduleId}):\\s*['"]([^'"]*)['"]`, 'g');
19
19
 
20
20
  if (entryRegex.test(content)) {
21
- // Update existing
22
- content = content.replace(entryRegex, `$1: '${url}'`);
21
+ // Update existing entry
22
+ content = content.replace(entryRegex, `'${moduleId}': '${url}'`);
23
23
  } else {
24
- // Insert new entry into devMode object
25
- const devModeRegex = /(devMode:\s*{)/;
26
- content = content.replace(devModeRegex, `$1\n '${moduleId}': '${url}',`);
24
+ // Insert new entry right after devMode: {
25
+ content = content.replace(/(devMode:\s*\{)/, `$1\n '${moduleId}': '${url}',`);
27
26
  }
28
27
 
29
28
  fs.writeFileSync(configPath, content);
@@ -44,9 +43,9 @@ const clearAllDevMode = (configPath) => {
44
43
  if (!fs.existsSync(configPath)) return;
45
44
  let content = fs.readFileSync(configPath, 'utf8');
46
45
 
47
- // Remove the entire devMode block
48
- const devModeBlockRegex = /\s*devMode:\s*{[\s\S]*?},?/g;
49
- content = content.replace(devModeBlockRegex, '');
46
+ // Simply reset devMode to empty object
47
+ const devModeBlockRegex = /devMode:\s*{[\s\S]*?}/;
48
+ content = content.replace(devModeBlockRegex, 'devMode: {}');
50
49
 
51
50
  fs.writeFileSync(configPath, content);
52
51
  };