@codemoreira/esad 1.4.6 → 2.0.0-rc.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/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.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",
@@ -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
+
@@ -8,12 +8,15 @@ const templatesConfig = require('../templates/templates.json');
8
8
  module.exports = async (moduleName) => {
9
9
  const configObj = getWorkspaceConfig();
10
10
  if (!configObj) {
11
- console.error(`❌ Error: Call this command from inside an ESAD workspace (esad.config.json not found).`);
11
+ console.error(`❌ Error: Call this command from inside an ESAD workspace (esad.config.js not found).`);
12
12
  return;
13
13
  }
14
14
 
15
- const { projectName } = configObj.data;
15
+ const config = await configObj.load();
16
+ const projectName = config.default?.projectName || config.projectName;
17
+
16
18
  const isPrefixed = moduleName.startsWith(`${projectName}-`);
19
+
17
20
  const finalModuleName = isPrefixed ? moduleName : `${projectName}-${moduleName}`;
18
21
 
19
22
  const workspaceDir = path.dirname(configObj.path);
@@ -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
+
@@ -10,34 +10,53 @@ module.exports = async (projectName) => {
10
10
 
11
11
  fs.ensureDirSync(workspaceDir);
12
12
 
13
- const configPath = path.join(workspaceDir, 'esad.config.json');
13
+ const configPath = path.join(workspaceDir, 'esad.config.js');
14
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`);
15
+ const configTemplate = `/**
16
+ * ESAD: Super App Configuration
17
+ */
18
+ export default {
19
+ projectName: '${projectName}',
20
+
21
+ // 1. Development Overrides
22
+ // Managed automatically by 'esad dev'
23
+ devMode: {},
24
+
25
+ // 2. Programmable Deployment
26
+ // Receives the compiled bundle.
27
+ async deploy(bundle, { version, moduleId, options }) {
28
+ console.log('🚀 Starting custom upload for ' + moduleId + '...');
29
+
30
+ // Example: Simple CDN V2 upload
31
+ // const response = await fetch('http://localhost:3000/api/admin/modules/' + moduleId + '/versions', {
32
+ // method: 'POST',
33
+ // body: bundle,
34
+ // headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
35
+ // });
36
+ // return await response.json();
37
+
38
+ return { status: 'mock_success', moduleId, version };
39
+ }
40
+ };
41
+ `;
42
+ fs.writeFileSync(configPath, configTemplate);
43
+ console.log(`✅ Generated programmable configuration: esad.config.js`);
23
44
  }
24
45
 
25
46
  const gitignorePath = path.join(workspaceDir, '.gitignore');
26
47
  if (!fs.existsSync(gitignorePath)) {
27
48
  const hostName = `${projectName}-host`;
28
49
  const gitignoreContent = `# ESAD Workspace Git Configuration\n` +
29
- `# Ignore everything by default\n` +
30
50
  `/*\n\n` +
31
- `# Exceptions: Track only the Host and Configs\n` +
32
51
  `!/${hostName}/\n` +
33
- `!/esad.config.json\n` +
52
+ `!/esad.config.js\n` +
34
53
  `!/.gitignore\n` +
35
- `\n# Ignore node_modules\n` +
36
- `node_modules/\n`;
54
+ `\nnode_modules/\n`;
37
55
  fs.writeFileSync(gitignorePath, gitignoreContent);
38
56
  console.log(`✅ Generated .gitignore`);
39
57
  }
40
58
 
59
+
41
60
  const hostName = `${projectName}-host`;
42
61
  const hostDir = path.join(workspaceDir, hostName);
43
62
 
@@ -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,44 @@
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 clearAllDevMode = (configPath) => {
33
+ if (!fs.existsSync(configPath)) return;
34
+ let content = fs.readFileSync(configPath, 'utf8');
35
+
36
+ // Remove the entire devMode block
37
+ const devModeBlockRegex = /\s*devMode:\s*{[\s\S]*?},?/g;
38
+ content = content.replace(devModeBlockRegex, '');
39
+
40
+ fs.writeFileSync(configPath, content);
41
+ };
42
+
43
+ module.exports = { updateDevMode, removeDevMode, clearAllDevMode };
44
+