@codemoreira/esad 1.4.6 → 2.0.0-rc.2

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
@@ -1,89 +1,82 @@
1
1
  #!/usr/bin/env node
2
-
3
- const { program } = require('commander');
4
- const pkg = require('../package.json');
5
-
6
- // Import Commands
7
- const initCommand = require('../src/cli/commands/init');
8
- const createModuleCommand = require('../src/cli/commands/createModule');
9
- const createCdnCommand = require('../src/cli/commands/createCdn');
10
- const deployCommand = require('../src/cli/commands/deploy');
11
- const devCommand = require('../src/cli/commands/dev');
12
- const hostCommand = require('../src/cli/commands/host');
13
- const buildCommand = require('../src/cli/commands/build');
14
-
15
- program
16
- .version(pkg.version)
17
- .description('esad - Easy Super App Development Toolkit');
18
-
19
- // --- COMMMAND: esad init ---
20
- program
21
- .command('init <project-name>')
22
- .description('Scaffold a new ESAD workspace containing the Host App')
23
- .action(async (name) => {
24
- await initCommand(name);
25
- process.exit(0);
26
- });
27
-
28
- // --- COMMAND: esad create-cdn ---
29
- program
30
- .command('create-cdn [cdn-name]')
31
- .description('Scaffold the CDN / Registry backend')
32
- .action(async (name) => {
33
- await createCdnCommand(name);
34
- process.exit(0);
35
- });
36
-
37
- // --- COMMAND: esad host ---
38
- program
39
- .command('host <subcommand>')
40
- .description('Manage the Host App (dev, android, ios)')
41
- .action(async (sub) => {
42
- await hostCommand(sub);
43
- process.exit(0);
44
- });
45
-
46
- // --- COMMAND: esad create-module ---
47
- program
48
- .command('create-module <module-name>')
49
- .description('Scaffold a React Native mini-app automatically configured for Module Federation via ESAD')
50
- .action(async (name) => {
51
- await createModuleCommand(name);
52
- process.exit(0);
53
- });
54
-
55
- // --- COMMAND: esad build ---
56
- program
57
- .command('build')
58
- .option('-i, --id <moduleId>', 'The Module ID to build')
59
- .option('-p, --platform <platform>', 'Platform to build for (android, ios)', 'android')
60
- .description('Builds a production bundle for the host or a specific module')
61
- .action(async (options) => {
62
- await buildCommand(options);
63
- process.exit(0);
64
- });
65
-
66
- // --- COMMAND: esad deploy ---
67
- program
68
- .command('deploy')
69
- .option('-v, --version <semver>', 'Version number (e.g., 1.0.0)')
70
- .option('-i, --id <moduleId>', 'The Module ID to deploy')
71
- .option('-e, --entry <entryFileName>', 'The name of the main entry bundle (e.g., index.bundle)', 'index.bundle')
72
- .description('Zips the local dist directory and uploads it to the configured deployment endpoint')
73
- .action(async (options) => {
74
- await deployCommand(options);
75
- process.exit(0);
76
- });
77
-
78
- // --- COMMAND: esad dev ---
79
- program
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')
84
- .action(async (options) => {
85
- await devCommand(options);
86
- // Note: dev command has its own shutdown logic with SIGINT/SIGTERM
87
- });
88
-
89
- program.parse(process.argv);
2
+
3
+ const { program } = require('commander');
4
+ const pkg = require('../package.json');
5
+
6
+ program
7
+ .version(pkg.version)
8
+ .description('esad - Easy Super App Development Toolkit (V2)');
9
+
10
+ // --- COMMAND: esad create [name] --type [host|module|cdn] ---
11
+ program
12
+ .command('create [name]')
13
+ .option('-t, --type <type>', 'Type of project: host, module, cdn', 'module')
14
+ .description('Unified scaffolding for host apps, modules, and cdn registries')
15
+ .action(async (name, options) => {
16
+ await require('../src/cli/commands/create')(name, options);
17
+ process.exit(0);
18
+ });
19
+
20
+ // --- COMMAND: esad dev [moduleId] ---
21
+ program
22
+ .command('dev [id]') // [id] as alias to -i for better UX
23
+ .option('-i, --id <moduleId>', 'The Module ID to run in dev mode')
24
+ .option('-p, --port <port>', 'The port to run the dev server on', '8081')
25
+ .description('Starts the dev server and updates the local mapping')
26
+ .action(async (id, options) => {
27
+ const opts = { ...options, id: id || options.id };
28
+ await require('../src/cli/commands/dev')(opts);
29
+ });
30
+
31
+ // --- COMMAND: esad build [id] ---
32
+ program
33
+ .command('build [id]')
34
+ .option('-i, --id <moduleId>', 'The Module ID to build')
35
+ .option('-p, --platform <platform>', 'Platform: android, ios', 'android')
36
+ .description('Builds a production bundle')
37
+ .action(async (id, options) => {
38
+ const opts = { ...options, id: id || options.id };
39
+ await require('../src/cli/commands/build')(opts);
40
+ process.exit(0);
41
+ });
42
+
43
+ // --- COMMAND: esad deploy [id] ---
44
+ program
45
+ .command('deploy [id]')
46
+ .option('-i, --id <moduleId>', 'The Module ID to deploy')
47
+ .option('-v, --version <version>', 'Specific version to deploy')
48
+ .description('Executes the programmable deploy hook')
49
+ .action(async (id, options) => {
50
+ const opts = { ...options, id: id || options.id };
51
+ await require('../src/cli/commands/deploy')(opts);
52
+ process.exit(0);
53
+ });
54
+
55
+ // --- COMMAND: esad host <sub> ---
56
+ program
57
+ .command('host <subcommand>')
58
+ .description('Manage host application (android, ios, login)')
59
+ .action(async (sub) => {
60
+ await require('../src/cli/commands/host')(sub);
61
+ process.exit(0);
62
+ });
63
+
64
+ // --- COMMAND: esad doctor ---
65
+ program
66
+ .command('doctor')
67
+ .description('Check environment for common issues')
68
+ .action(async () => {
69
+ await require('../src/cli/commands/doctor')();
70
+ process.exit(0);
71
+ });
72
+
73
+ // --- COMMAND: esad link [id] ---
74
+ program
75
+ .command('link [id]')
76
+ .description('Optimize development via local filesystem linking')
77
+ .action(async (id) => {
78
+ await require('../src/cli/commands/link')(id);
79
+ process.exit(0);
80
+ });
81
+
82
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemoreira/esad",
3
- "version": "1.4.6",
3
+ "version": "2.0.0-rc.2",
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",
@@ -3,40 +3,41 @@ const path = require('path');
3
3
  const fs = require('fs-extra');
4
4
  const chalk = require('chalk');
5
5
  const { getWorkspaceConfig } = require('../utils/config');
6
- const { resolveProjectDir, listAvailableModules } = require('../utils/resolution');
6
+ const { resolveProjectDir } = require('../utils/resolution');
7
+ const { clearAllDevMode } = require('../utils/transformer');
7
8
 
8
9
  module.exports = async (options) => {
9
- let cwd = process.cwd();
10
-
11
- // Enforce Workspace Root
12
10
  const configObj = getWorkspaceConfig();
13
11
  if (!configObj) {
14
- console.error(chalk.red(`❌ Error: Call this command from the project root (esad.config.json not found).`));
12
+ console.error(chalk.red(`❌ Error: esad.config.js not found.`));
15
13
  process.exit(1);
16
14
  }
17
15
 
18
- const { projectName } = configObj.data;
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();
19
21
 
20
22
  if (options.id) {
21
23
  const targetDir = resolveProjectDir(options.id, configObj);
22
24
  if (!targetDir) {
23
- console.error(chalk.red(`\n❌ Error: Module not found: ${options.id}`));
24
- listAvailableModules(configObj);
25
+ console.error(chalk.red(`❌ Error: Module not found: ${options.id}`));
25
26
  process.exit(1);
26
27
  }
27
28
  cwd = targetDir;
28
- } else {
29
- // Build host by default if in root
30
- const hostDir = path.join(path.dirname(configObj.path), `${projectName}-host`);
31
- if (fs.existsSync(hostDir)) cwd = hostDir;
32
29
  }
33
30
 
34
31
  const platform = options.platform || 'android';
35
32
 
36
- console.log(`\n🏗️ Building production bundle for ${path.basename(cwd)} (${platform})...\n`);
33
+ console.log(`\n🏗️ Building production bundle for ${chalk.cyan(path.basename(cwd))} (${platform})...\n`);
37
34
 
35
+ // 1. CLEANUP CONFIG (Avoid shipping local dev URLs)
36
+ console.log(chalk.gray(`🧹 Cleaning up devMode mappings in esad.config.js...`));
37
+ clearAllDevMode(configObj.path);
38
+
38
39
  try {
39
- const bundleOutput = path.join(cwd, 'build', platform, 'index.bundle');
40
+ const bundleOutput = path.join(cwd, 'build', 'index.bundle'); // Simplified path as per V2
40
41
  fs.ensureDirSync(path.dirname(bundleOutput));
41
42
 
42
43
  // Run Re.Pack production build
@@ -47,12 +48,13 @@ module.exports = async (options) => {
47
48
  '--dev', 'false',
48
49
  '--bundle-output', bundleOutput,
49
50
  '--assets-dest', path.dirname(bundleOutput)
50
- ], cwd);
51
+ ], { cwd });
51
52
 
52
53
  console.log(chalk.green(`\n✅ Build complete! Assets generated in build/ directory.`));
53
- console.log(`👉 You can now run: esad deploy ${options.id ? `--id ${options.id}` : ''}\n`);
54
+ console.log(`👉 Next step: 'esad deploy ${options.id || ''}'\n`);
54
55
  } catch (err) {
55
56
  console.error(chalk.red(`\n❌ Build failed: ${err.message}`));
56
57
  process.exit(1);
57
58
  }
58
59
  };
60
+
@@ -0,0 +1,153 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { runProcess } = require('../utils/process');
5
+ const { cloneTemplate, renameProject } = require('../utils/scaffold');
6
+ const { getWorkspaceConfig } = require('../utils/config');
7
+ const templatesConfig = require('../templates/templates.json');
8
+
9
+ const initHost = async (projectName) => {
10
+ const workspaceDir = path.join(process.cwd(), projectName);
11
+ console.log(`\n🚀 Initializing ESAD Workspace: ${projectName}...\n`);
12
+
13
+ fs.ensureDirSync(workspaceDir);
14
+
15
+ const configPath = path.join(workspaceDir, 'esad.config.js');
16
+ if (!fs.existsSync(configPath)) {
17
+ const configTemplate = `/**
18
+ * ESAD: Super App Configuration
19
+ */
20
+ export default {
21
+ projectName: '${projectName}',
22
+
23
+ // 1. Development Overrides
24
+ // Managed automatically by 'esad dev'
25
+ devMode: {},
26
+
27
+ // 2. Programmable Deployment
28
+ // Receives the compiled bundle.
29
+ async deploy(bundle, { version, moduleId, options }) {
30
+ console.log('🚀 Starting custom upload for ' + moduleId + '...');
31
+ // return { status: 'mock_success', moduleId, version };
32
+ }
33
+ };
34
+ `;
35
+ fs.writeFileSync(configPath, configTemplate);
36
+ console.log(`✅ Generated programmable configuration: esad.config.js`);
37
+ }
38
+
39
+ const gitignorePath = path.join(workspaceDir, '.gitignore');
40
+ if (!fs.existsSync(gitignorePath)) {
41
+ const hostName = `${projectName}-host`;
42
+ const gitignoreContent = `# ESAD Workspace Git Configuration\n` +
43
+ `/*\n\n` +
44
+ `!/${hostName}/\n` +
45
+ `!/esad.config.js\n` +
46
+ `!/.gitignore\n` +
47
+ `\nnode_modules/\n`;
48
+ fs.writeFileSync(gitignorePath, gitignoreContent);
49
+ console.log(`✅ Generated .gitignore`);
50
+ }
51
+
52
+ const hostName = `${projectName}-host`;
53
+ const hostDir = path.join(workspaceDir, hostName);
54
+
55
+ try {
56
+ await cloneTemplate(templatesConfig.host, hostDir);
57
+ await renameProject(hostDir, hostName);
58
+ console.log(`\n📦 Installing dependencies into host...`);
59
+ await runProcess('npm', ['install'], { cwd: hostDir });
60
+ console.log(`\n🎉 ESAD Workspace Initialized!`);
61
+ console.log(`-> cd ${projectName}/${hostName}\n-> esad host dev (to start Host)`);
62
+ } catch (err) {
63
+ console.error(`❌ Failed to init Host:`, err.message);
64
+ }
65
+ };
66
+
67
+ const createModule = async (moduleName) => {
68
+ const configObj = getWorkspaceConfig();
69
+ if (!configObj) {
70
+ console.error(`❌ Error: Call this command from inside an ESAD workspace (esad.config.js not found).`);
71
+ return;
72
+ }
73
+
74
+ const config = await configObj.load();
75
+ const projectName = config.default?.projectName || config.projectName;
76
+
77
+ const isPrefixed = moduleName.startsWith(`${projectName}-`);
78
+ const finalModuleName = isPrefixed ? moduleName : `${projectName}-${moduleName}`;
79
+
80
+ const workspaceDir = configObj.root;
81
+ const targetDir = path.join(workspaceDir, finalModuleName);
82
+
83
+ console.log(`\n📦 Creating federated mini-app: ${finalModuleName}...\n`);
84
+
85
+ try {
86
+ await cloneTemplate(templatesConfig.module, targetDir);
87
+ await renameProject(targetDir, finalModuleName);
88
+ console.log(`\n📦 Installing dependencies...`);
89
+ await runProcess('npm', ['install'], { cwd: targetDir });
90
+ console.log(`\n🎉 Module ${finalModuleName} is ready!`);
91
+ } catch (err) {
92
+ console.error(`❌ Failed to scaffold module`, err.message);
93
+ }
94
+ };
95
+
96
+ const createCdn = async (cdnName) => {
97
+ const configObj = getWorkspaceConfig();
98
+ if (!configObj) {
99
+ console.error(`❌ Error: Call this command from inside an ESAD workspace (esad.config.js not found).`);
100
+ return;
101
+ }
102
+
103
+ const config = await configObj.load();
104
+ const projectName = config.default?.projectName || config.projectName;
105
+
106
+ const finalCdnName = cdnName || `${projectName}-cdn`;
107
+ const cdnPath = path.join(process.cwd(), finalCdnName);
108
+
109
+ if (fs.existsSync(cdnPath)) {
110
+ console.error(`❌ Error: Directory ./${finalCdnName} already exists.`);
111
+ return;
112
+ }
113
+
114
+ console.log(`\n📦 Initializing Flux Registry & CDN: ${finalCdnName}...\n`);
115
+
116
+ try {
117
+ console.log(`📡 Cloning template from GitHub...`);
118
+ await runProcess('git', ['clone', 'https://github.com/CodeMoreira/simple-cdn.git', finalCdnName]);
119
+ console.log(`🧹 Cleaning up template metadata...`);
120
+ await fs.remove(path.join(cdnPath, '.git'));
121
+ console.log(`\n📥 Installing dependencies (this may take a minute)...`);
122
+ await runProcess('npm', ['install'], { cwd: cdnPath });
123
+ console.log(`\n✅ CDN Registry created successfully in ./${finalCdnName}\n`);
124
+ } catch (error) {
125
+ console.error(`\n❌ Failed to create CDN: ${error.message}`);
126
+ }
127
+ };
128
+
129
+ module.exports = async (name, options) => {
130
+ const type = options.type;
131
+
132
+ if (type === 'host') {
133
+ if (!name) {
134
+ console.error(chalk.red('❌ Error: Project name is required for host creation.'));
135
+ process.exit(1);
136
+ }
137
+ return await initHost(name);
138
+ }
139
+
140
+ if (type === 'module') {
141
+ if (!name) {
142
+ console.error(chalk.red('❌ Error: Module name is required. Usage: esad create [name] --type module'));
143
+ process.exit(1);
144
+ }
145
+ return await createModule(name);
146
+ }
147
+
148
+ if (type === 'cdn') {
149
+ return await createCdn(name);
150
+ }
151
+
152
+ console.error(chalk.red(`❌ Unknown type: ${type}. Valid types are: host, module, cdn.`));
153
+ };
@@ -3,110 +3,78 @@ const path = require('path');
3
3
  const AdmZip = require('adm-zip');
4
4
  const chalk = require('chalk');
5
5
  const { getWorkspaceConfig } = require('../utils/config');
6
- const { resolveModuleMetadata, listAvailableModules } = require('../utils/resolution');
6
+ const { resolveModuleMetadata } = require('../utils/resolution');
7
7
 
8
8
  module.exports = async (options) => {
9
- let cwd = process.cwd();
10
- let pkgPath = path.join(cwd, 'package.json');
11
-
12
- // Enforce Workspace Root
13
9
  const configObj = getWorkspaceConfig();
14
10
  if (!configObj) {
15
- console.error(chalk.red(`❌ Error: Call this command from the project root (esad.config.json not found).`));
11
+ console.error(chalk.red(`❌ Error: esad.config.js not found in this or parent directories.`));
16
12
  process.exit(1);
17
13
  }
18
14
 
19
- const workspaceRoot = path.dirname(configObj.path);
20
- const { projectName } = configObj.data;
15
+ const config = await configObj.load();
16
+ const workspaceRoot = configObj.root;
17
+ const projectName = config.default?.projectName || config.projectName;
21
18
 
22
19
  let moduleId = options.id;
23
-
20
+ let cwd = process.cwd();
21
+
22
+ // Resolve Context
24
23
  if (moduleId) {
25
24
  const meta = resolveModuleMetadata(moduleId, configObj);
26
25
  if (!meta) {
27
- console.error(chalk.red(`\n❌ Error: Module not found: ${moduleId}`));
28
- listAvailableModules(configObj);
26
+ console.error(chalk.red(`❌ Error: Module not found: ${moduleId}`));
29
27
  process.exit(1);
30
28
  }
31
29
  cwd = meta.path;
32
- moduleId = meta.id; // Correct fully qualified ID
33
- pkgPath = path.join(cwd, 'package.json');
34
- console.log(`📂 Module detected for Deploy: ${path.basename(cwd)}`);
35
- } else {
36
- // Target host by default if in root
37
- const hostDir = path.join(workspaceRoot, `${projectName}-host`);
38
- if (fs.existsSync(hostDir)) {
39
- cwd = hostDir;
40
- pkgPath = path.join(cwd, 'package.json');
41
- console.log(chalk.green(`📂 Host detected for Deploy: ${path.basename(cwd)}`));
42
- }
43
30
  }
44
31
 
32
+ const pkgPath = path.join(cwd, 'package.json');
45
33
  if (!fs.existsSync(pkgPath)) {
46
34
  console.error(chalk.red(`❌ Error: package.json not found in ${cwd}.`));
47
35
  process.exit(1);
48
36
  }
49
37
 
50
38
  const pkg = fs.readJsonSync(pkgPath);
51
- moduleId = moduleId || pkg.name;
39
+ const resolvedModuleId = moduleId || pkg.name;
52
40
  const version = options.version || pkg.version;
53
- const entry = options.entry || 'index.bundle';
54
41
 
55
- console.log(`\n☁️ Starting ESAD Deploy for ${moduleId} (v${version})\n`);
56
-
57
- const config = configObj ? configObj.data : null;
58
- if (!config?.deployEndpoint) {
59
- console.error(`❌ Error: 'deployEndpoint' not configured in esad.config.json.`);
60
- process.exit(1);
61
- }
62
-
63
- const deployUrl = config.deployEndpoint.replace('{{moduleId}}', moduleId);
64
- console.log(`📡 Deployment Endpoint Resolved: ${deployUrl}`);
65
-
42
+ console.log(`\n🚀 Starting ESAD Deploy for ${chalk.cyan(resolvedModuleId)} (v${version})\n`);
43
+
66
44
  const distPath = path.join(cwd, 'build');
67
45
  if (!fs.existsSync(distPath)) {
68
- console.error(`❌ Error: build/ directory not found in ${cwd}. Did you run the build command?`);
46
+ console.error(chalk.red(`❌ Error: build/ directory not found. Please run 'esad build' first.`));
69
47
  process.exit(1);
70
48
  }
71
49
 
50
+ // ZIP BUNDLE
72
51
  const zip = new AdmZip();
73
52
  zip.addLocalFolder(distPath);
53
+ const buffer = zip.toBuffer();
74
54
 
75
- const zipPath = path.join(cwd, `bundle-${moduleId}-${version}.zip`);
76
- zip.writeZip(zipPath);
77
- console.log(`🗜️ Zipped output to ${zipPath}`);
55
+ console.log(`🗜️ Generated bundle zip (${(buffer.length / 1024).toFixed(2)} KB)`);
78
56
 
79
- console.log(`🚀 Uploading to CDN via multipart POST...`);
57
+ // RUN DEPLOY HOOK
58
+ const deployHook = config.default?.deploy || config.deploy;
80
59
 
81
- try {
82
- const FormData = require('form-data'); // Standard in Node versions, or use native if available
83
- const form = new FormData();
84
- form.append('version', version);
85
- form.append('bundle', fs.createReadStream(zipPath));
86
-
87
- const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
88
-
89
- // Simple CDN expects POST /api/admin/assets/:id/versions
90
- const uploadUrl = deployUrl.includes('/versions') ? deployUrl : `${deployUrl}/versions`;
60
+ if (typeof deployHook !== 'function') {
61
+ console.error(chalk.red(`❌ Error: 'deploy' function not found in esad.config.js.`));
62
+ process.exit(1);
63
+ }
91
64
 
92
- const response = await fetch(uploadUrl, {
93
- method: 'POST',
94
- body: form,
95
- headers: form.getHeaders(),
65
+ try {
66
+ console.log(`📡 Invoking custom 'deploy' hook...`);
67
+ const result = await deployHook(buffer, {
68
+ version,
69
+ moduleId: resolvedModuleId,
70
+ options
96
71
  });
97
-
98
- if (response.ok) {
99
- const result = await response.json();
100
- console.log(chalk.green(`✅ Successfully uploaded ${moduleId} v${version} to CDN!`));
101
- console.log(`📄 Active Version is now: ${result.active_version}`);
102
- } else {
103
- const errorText = await response.text();
104
- console.error(chalk.red(`❌ Failed to upload: ${response.status} ${response.statusText}`));
105
- console.error(errorText);
106
- }
72
+
73
+ console.log(chalk.green(`\n✅ Deployment successful!`));
74
+ if (result) console.log(JSON.stringify(result, null, 2));
107
75
  } catch (err) {
108
- console.error(chalk.red(`❌ Error during upload: ${err.message}`));
109
- } finally {
110
- fs.unlinkSync(zipPath);
76
+ console.error(chalk.red(`\n❌ Deployment failed: ${err.message}`));
77
+ process.exit(1);
111
78
  }
112
79
  };
80
+
@@ -1,89 +1,70 @@
1
- const { spawn } = require('cross-spawn');
1
+ const { runProcess } = require('../utils/process');
2
2
  const { getWorkspaceConfig } = require('../utils/config');
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const chalk = require('chalk');
6
6
  const { prepareNative } = require('../utils/scaffold');
7
- const { resolveProjectDir, listAvailableModules } = require('../utils/resolution');
7
+ const { resolveProjectDir } = require('../utils/resolution');
8
8
 
9
9
  module.exports = async (options) => {
10
- let cwd = process.cwd();
11
- let pkgPath = path.join(cwd, 'package.json');
12
-
13
- // Enforce Workspace Root
14
10
  const configObj = getWorkspaceConfig();
15
11
  if (!configObj) {
16
- console.error(chalk.red(`❌ Error: Call this command from the project root (esad.config.json not found).`));
12
+ console.error(chalk.red(`❌ Error: esad.config.js not found. Run this from your project root.`));
17
13
  process.exit(1);
18
14
  }
19
15
 
20
- const workspaceRoot = path.dirname(configObj.path);
21
- const { projectName } = configObj.data;
16
+ const config = await configObj.load();
17
+ const workspaceRoot = configObj.root;
18
+ const projectName = config.default?.projectName || config.projectName;
22
19
 
23
- if (options.id) {
24
- const targetDir = resolveProjectDir(options.id, configObj);
20
+ let cwd = process.cwd();
21
+ let selectedModuleId = options.id;
22
+
23
+ if (selectedModuleId) {
24
+ const targetDir = resolveProjectDir(selectedModuleId, configObj);
25
25
  if (!targetDir) {
26
- console.error(chalk.red(`\n❌ Error: Module not found: ${options.id}`));
27
- listAvailableModules(configObj);
26
+ console.error(chalk.red(`❌ Error: Module not found: ${selectedModuleId}`));
28
27
  process.exit(1);
29
28
  }
30
29
  cwd = targetDir;
31
- pkgPath = path.join(cwd, 'package.json');
32
- console.log(chalk.green(`📂 Module detected: ${path.relative(workspaceRoot, cwd)}`));
33
- } else {
34
- // Target host by default if in root
35
- const hostDir = path.join(workspaceRoot, `${projectName}-host`);
36
- if (fs.existsSync(hostDir)) {
37
- cwd = hostDir;
38
- pkgPath = path.join(cwd, 'package.json');
39
- console.log(chalk.green(`📂 Host detected: ${path.relative(workspaceRoot, cwd)}`));
40
- }
41
30
  }
42
31
 
32
+ const pkgPath = path.join(cwd, 'package.json');
43
33
  const pkg = fs.readJsonSync(pkgPath);
44
- const moduleId = options.id || pkg.name;
34
+ const moduleId = selectedModuleId || pkg.name;
45
35
  const port = options.port || '8081';
46
36
 
47
37
  // Determine if it's a Host or Module
48
38
  const isHost = pkg.name.endsWith('-host') || pkg.dependencies?.['@callstack/repack'];
49
39
 
50
- // 1. Initial Checks & Automated Native Preparation
51
40
  await prepareNative(cwd, 'all');
52
41
 
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 });
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 });
56
45
  return;
57
46
  }
58
47
 
59
- console.log(`\n⚡ Starting ESAD Dev Server for ${moduleId} on port ${port}...\n`);
48
+ const { updateDevMode, removeDevMode } = require('../utils/transformer');
60
49
 
61
- const config = configObj ? configObj.data : null;
62
- let devApiUrl = config?.devModeEndpoint ? config.devModeEndpoint.replace('{{moduleId}}', moduleId) : null;
63
-
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
- };
75
-
76
- await setDevMode(true);
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
+ console.log(chalk.gray(`📡 Mode: Module Dev. Host configured to load ${moduleId} from ${localBundleUrl}`));
77
56
 
78
- const proc = spawn('npx', ['react-native', 'webpack-start', '--port', port], { cwd, stdio: 'inherit', shell: true });
57
+ const proc = runProcess('npx', ['react-native', 'webpack-start', '--port', port], { cwd });
79
58
 
80
59
  const shutdown = async () => {
81
- console.log(`\n🛑 Stopping ESAD Dev Server and reverting registry status...`);
82
- await setDevMode(false);
83
- proc.kill();
60
+ console.log(`\n🛑 Stopping ESAD Dev Server and reverting config...`);
61
+ removeDevMode(configObj.path, moduleId);
62
+ if (proc.kill) proc.kill();
84
63
  process.exit(0);
85
64
  };
86
65
 
66
+
87
67
  process.on('SIGINT', shutdown);
88
68
  process.on('SIGTERM', shutdown);
89
69
  };
70
+
@@ -0,0 +1,72 @@
1
+ const { getWorkspaceConfig } = require('../utils/config');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+
6
+ module.exports = async () => {
7
+ console.log(`\n🏥 ${chalk.bold('ESAD Doctor: Environment Diagnostics')}\n`);
8
+
9
+ const configObj = getWorkspaceConfig();
10
+ let errors = 0;
11
+ let warnings = 0;
12
+
13
+ // 1. Config Validation
14
+ if (!configObj) {
15
+ console.error(`${chalk.red('✖')} esad.config.js not found in any parent directories.`);
16
+ errors++;
17
+ } else {
18
+ console.log(`${chalk.green('✔')} esad.config.js detected at: ${chalk.gray(configObj.path)}`);
19
+
20
+ // 2. Deployment Hook Validation
21
+ const config = await configObj.load();
22
+ const deployHook = config.default?.deploy || config.deploy;
23
+ if (typeof deployHook === 'function') {
24
+ console.log(`${chalk.green('✔')} Generic 'deploy' hook is properly defined.`);
25
+ } else {
26
+ console.warn(`${chalk.yellow('⚠')} Warning: 'deploy' hook is missing or is not a function.`);
27
+ warnings++;
28
+ }
29
+ }
30
+
31
+ // 3. Registry & Auth Check
32
+ const envPath = path.join(process.cwd(), '.env');
33
+ if (fs.existsSync(envPath)) {
34
+ const envContent = fs.readFileSync(envPath, 'utf8');
35
+ if (envContent.includes('registryUrl')) {
36
+ console.log(`${chalk.green('✔')} registryUrl found in .env`);
37
+ } else {
38
+ console.warn(`${chalk.yellow('⚠')} Warning: registryUrl not found in .env (Host might fail to resolve modules).`);
39
+ warnings++;
40
+ }
41
+ } else {
42
+ console.warn(`${chalk.yellow('⚠')} Warning: .env file not found in current directory.`);
43
+ warnings++;
44
+ }
45
+
46
+ // 4. Dependency Sync Check (Simple version)
47
+ const pkgPath = path.join(process.cwd(), 'package.json');
48
+ if (fs.existsSync(pkgPath)) {
49
+ const pkg = fs.readJsonSync(pkgPath);
50
+ const deps = pkg.dependencies || {};
51
+
52
+ const criticalDeps = ['react-native', '@callstack/repack', 'react'];
53
+ criticalDeps.forEach(dep => {
54
+ if (deps[dep]) {
55
+ console.log(`${chalk.green('✔')} Found critical dependency: ${dep} (${deps[dep]})`);
56
+ } else {
57
+ console.warn(`${chalk.yellow('⚠')} Missing critical dependency in this context: ${dep}`);
58
+ warnings++;
59
+ }
60
+ });
61
+ }
62
+
63
+ console.log(`\n------------------------------------------------`);
64
+ if (errors === 0) {
65
+ console.log(`${chalk.bold.green('STABLE')}: ${errors} errors, ${warnings} warnings.`);
66
+ if (warnings > 0) console.log(`👉 Consider addressing the warnings to ensure a perfect developer experience.`);
67
+ } else {
68
+ console.log(`${chalk.bold.red('UNSTABLE')}: ${errors} errors, ${warnings} warnings.`);
69
+ console.log(`👉 Run 'esad init' or fix the manual errors above.`);
70
+ }
71
+ console.log(`------------------------------------------------\n`);
72
+ };
@@ -0,0 +1,35 @@
1
+ const { getWorkspaceConfig } = require('../utils/config');
2
+ const { resolveProjectDir } = require('../utils/resolution');
3
+ const { updateDevMode } = require('../utils/transformer');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ module.exports = async (moduleId) => {
8
+ const configObj = getWorkspaceConfig();
9
+ if (!configObj) {
10
+ console.error(chalk.red(`❌ Error: esad.config.js not found.`));
11
+ process.exit(1);
12
+ }
13
+
14
+ if (!moduleId) {
15
+ console.error(chalk.red(`❌ Error: Module ID is required for linking. Usage: esad link [module-name]`));
16
+ process.exit(1);
17
+ }
18
+
19
+ const targetDir = resolveProjectDir(moduleId, configObj);
20
+ if (!targetDir) {
21
+ console.error(chalk.red(`❌ Error: Module not found: ${moduleId}`));
22
+ process.exit(1);
23
+ }
24
+
25
+ // Generate a local filesystem link
26
+ // Re.Pack can handle file:// on some platforms, or we use this as a hint for the resolver.
27
+ const localPath = `file://${path.join(targetDir, 'build', 'index.bundle')}`;
28
+
29
+ updateDevMode(configObj.path, moduleId, localPath);
30
+
31
+ console.log(`\n🔗 ${chalk.bold('Module Linked Locally')}`);
32
+ console.log(`${chalk.cyan(moduleId)} -> ${chalk.gray(localPath)}`);
33
+ console.log(`\n👉 Run 'esad build ${moduleId}' to generate the bundle if you haven't yet.`);
34
+ console.log(`📡 The Host app's ScriptManager will now prioritize this local file.\n`);
35
+ };
@@ -1,11 +1,40 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
+ const { createJiti } = require('jiti');
3
4
 
4
5
  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');
7
- if (!fs.existsSync(configPath)) return null;
8
- return { path: configPath, data: fs.readJsonSync(configPath) };
6
+ let current = process.cwd();
7
+ let configPath = null;
8
+
9
+ // Search upwards for esad.config.js
10
+ while (current !== path.parse(current).root) {
11
+ const check = path.join(current, 'esad.config.js');
12
+ if (fs.existsSync(check)) {
13
+ configPath = check;
14
+ break;
15
+ }
16
+ current = path.dirname(current);
17
+ }
18
+
19
+ if (!configPath) return null;
20
+
21
+ try {
22
+ const jiti = createJiti(__filename);
23
+ const configModule = jiti.import(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
+
28
+ return {
29
+ path: configPath,
30
+ root: path.dirname(configPath),
31
+ load: () => configModule
32
+ };
33
+ } catch (err) {
34
+ console.error(`❌ Failed to load esad.config.js: ${err.message}`);
35
+ return null;
36
+ }
9
37
  };
10
38
 
11
39
  module.exports = { getWorkspaceConfig };
40
+
@@ -0,0 +1,54 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Safely updates the devMode object in esad.config.js
6
+ * This uses a simple yet robust regex approach to preserve comments/formatting.
7
+ */
8
+ const updateDevMode = (configPath, moduleId, url) => {
9
+ let content = fs.readFileSync(configPath, 'utf8');
10
+
11
+ // 1. Ensure devMode object exists
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`);
15
+ }
16
+
17
+ // 2. Add or update the module entry
18
+ const entryRegex = new RegExp(`(['"]${moduleId}['"]|${moduleId}):\\s*['"]([^'"]*)['"]`, 'g');
19
+
20
+ if (entryRegex.test(content)) {
21
+ // Update existing
22
+ content = content.replace(entryRegex, `$1: '${url}'`);
23
+ } else {
24
+ // Insert new entry into devMode object
25
+ const devModeRegex = /(devMode:\s*{)/;
26
+ content = content.replace(devModeRegex, `$1\n '${moduleId}': '${url}',`);
27
+ }
28
+
29
+ fs.writeFileSync(configPath, content);
30
+ };
31
+
32
+ const removeDevMode = (configPath, moduleId) => {
33
+ if (!fs.existsSync(configPath)) return;
34
+ let content = fs.readFileSync(configPath, 'utf8');
35
+
36
+ // Remove specific module entry
37
+ const entryRegex = new RegExp(`\\s*['"]?${moduleId}['"]?:\\s*['"]([^'"]*)['"],?`, 'g');
38
+ content = content.replace(entryRegex, '');
39
+
40
+ fs.writeFileSync(configPath, content);
41
+ };
42
+
43
+ const clearAllDevMode = (configPath) => {
44
+ if (!fs.existsSync(configPath)) return;
45
+ let content = fs.readFileSync(configPath, 'utf8');
46
+
47
+ // Remove the entire devMode block
48
+ const devModeBlockRegex = /\s*devMode:\s*{[\s\S]*?},?/g;
49
+ content = content.replace(devModeBlockRegex, '');
50
+
51
+ fs.writeFileSync(configPath, content);
52
+ };
53
+
54
+ module.exports = { updateDevMode, removeDevMode, clearAllDevMode };
@@ -1,44 +0,0 @@
1
- const { runProcess } = require('../utils/process');
2
- const { getWorkspaceConfig } = require('../utils/config');
3
- const fs = require('fs-extra');
4
- const path = require('path');
5
-
6
- module.exports = async (cdnName) => {
7
- const configObj = getWorkspaceConfig();
8
- if (!configObj) {
9
- console.error(`❌ Error: Call this command from inside an ESAD workspace (esad.config.json not found).`);
10
- return;
11
- }
12
-
13
- const finalCdnName = cdnName || `${configObj.data.projectName}-cdn`;
14
- const cdnPath = path.join(process.cwd(), finalCdnName);
15
-
16
- if (fs.existsSync(cdnPath)) {
17
- console.error(`❌ Error: Directory ./${finalCdnName} already exists.`);
18
- return;
19
- }
20
-
21
- console.log(`\n📦 Initializing Flux Registry & CDN: ${finalCdnName}...\n`);
22
-
23
- try {
24
- // 1. Clone the template
25
- console.log(`📡 Cloning template from GitHub...`);
26
- await runProcess('git', ['clone', 'https://github.com/CodeMoreira/simple-cdn.git', finalCdnName]);
27
-
28
- // 2. Remove .git from template
29
- console.log(`🧹 Cleaning up template metadata...`);
30
- await fs.remove(path.join(cdnPath, '.git'));
31
-
32
- // 3. Install dependencies
33
- console.log(`\n📥 Installing dependencies (this may take a minute)...`);
34
- await runProcess('npm', ['install'], cdnPath);
35
-
36
- console.log(`\n✅ CDN Registry created successfully in ./${finalCdnName}\n`);
37
- console.log(`To start your CDN:`);
38
- console.log(` cd ${finalCdnName}`);
39
- console.log(` npm start\n`);
40
-
41
- } catch (error) {
42
- console.error(`\n❌ Failed to create CDN: ${error.message}`);
43
- }
44
- };
@@ -1,38 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const { runProcess } = require('../utils/process');
4
- const { getWorkspaceConfig } = require('../utils/config');
5
- const { cloneTemplate, renameProject } = require('../utils/scaffold');
6
- const templatesConfig = require('../templates/templates.json');
7
-
8
- module.exports = async (moduleName) => {
9
- const configObj = getWorkspaceConfig();
10
- if (!configObj) {
11
- console.error(`❌ Error: Call this command from inside an ESAD workspace (esad.config.json not found).`);
12
- return;
13
- }
14
-
15
- const { projectName } = configObj.data;
16
- const isPrefixed = moduleName.startsWith(`${projectName}-`);
17
- const finalModuleName = isPrefixed ? moduleName : `${projectName}-${moduleName}`;
18
-
19
- const workspaceDir = path.dirname(configObj.path);
20
- const targetDir = path.join(workspaceDir, finalModuleName);
21
-
22
- console.log(`\n📦 Creating federated mini-app: ${finalModuleName}...\n`);
23
-
24
- try {
25
- // 1. Clone Template instead of react-native init
26
- await cloneTemplate(templatesConfig.module, targetDir);
27
-
28
- // 2. Rename Project
29
- await renameProject(targetDir, finalModuleName);
30
-
31
- console.log(`\n📦 Installing dependencies...`);
32
- await runProcess('npm', ['install'], targetDir);
33
-
34
- console.log(`\n🎉 Module ${finalModuleName} is ready!`);
35
- } catch (err) {
36
- console.error(`❌ Failed to scaffold module`, err.message);
37
- }
38
- };
@@ -1,59 +0,0 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const { runProcess } = require('../utils/process');
4
- const { cloneTemplate, renameProject } = require('../utils/scaffold');
5
- const templatesConfig = require('../templates/templates.json');
6
-
7
- module.exports = async (projectName) => {
8
- const workspaceDir = path.join(process.cwd(), projectName);
9
- console.log(`\n🚀 Initializing ESAD Workspace: ${projectName}...\n`);
10
-
11
- fs.ensureDirSync(workspaceDir);
12
-
13
- const configPath = path.join(workspaceDir, 'esad.config.json');
14
- if (!fs.existsSync(configPath)) {
15
- const configTemplate = {
16
- projectName: projectName,
17
- registryUrl: "http://localhost:3000/modules",
18
- deployEndpoint: "http://localhost:3000/api/admin/modules/{{moduleId}}/versions",
19
- devModeEndpoint: "http://localhost:3000/api/admin/modules/{{moduleId}}"
20
- };
21
- fs.writeJsonSync(configPath, configTemplate, { spaces: 2 });
22
- console.log(`✅ Generated dynamic configuration file: esad.config.json`);
23
- }
24
-
25
- const gitignorePath = path.join(workspaceDir, '.gitignore');
26
- if (!fs.existsSync(gitignorePath)) {
27
- const hostName = `${projectName}-host`;
28
- const gitignoreContent = `# ESAD Workspace Git Configuration\n` +
29
- `# Ignore everything by default\n` +
30
- `/*\n\n` +
31
- `# Exceptions: Track only the Host and Configs\n` +
32
- `!/${hostName}/\n` +
33
- `!/esad.config.json\n` +
34
- `!/.gitignore\n` +
35
- `\n# Ignore node_modules\n` +
36
- `node_modules/\n`;
37
- fs.writeFileSync(gitignorePath, gitignoreContent);
38
- console.log(`✅ Generated .gitignore`);
39
- }
40
-
41
- const hostName = `${projectName}-host`;
42
- const hostDir = path.join(workspaceDir, hostName);
43
-
44
- try {
45
- // 1. Clone Template instead of create-expo-app
46
- await cloneTemplate(templatesConfig.host, hostDir);
47
-
48
- // 2. Rename Project
49
- await renameProject(hostDir, hostName);
50
-
51
- console.log(`\n📦 Installing dependencies into host...`);
52
- await runProcess('npm', ['install'], hostDir);
53
-
54
- console.log(`\n🎉 ESAD Workspace Initialized!`);
55
- console.log(`-> cd ${projectName}/${hostName}\n-> esad host dev (to start Host)`);
56
- } catch (err) {
57
- console.error(`❌ Failed to init Host:`, err.message);
58
- }
59
- };