@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 +19 -5
- package/bin/esad.js +6 -6
- package/package.json +1 -1
- package/src/cli/commands/build.js +3 -1
- package/src/cli/commands/createCdn.js +2 -1
- package/src/cli/commands/deploy.js +3 -1
- package/src/cli/commands/dev.js +73 -39
- package/src/cli/utils/config.js +29 -3
- package/src/client/index.js +27 -14
- package/src/plugin/index.js +6 -2
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 &
|
|
27
|
-
Starts the Rspack server
|
|
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
|
|
30
|
-
|
|
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
|
|
31
|
-
.description('Scaffold the CDN
|
|
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 (
|
|
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
|
|
82
|
-
.option('-p, --
|
|
83
|
-
.description('
|
|
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
|
@@ -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
|
|
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) {
|
package/src/cli/commands/dev.js
CHANGED
|
@@ -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 {
|
|
7
|
-
const {
|
|
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
|
-
|
|
45
|
-
const
|
|
53
|
+
moduleId = moduleId || pkg.name;
|
|
54
|
+
const platform = options.platform || 'android';
|
|
46
55
|
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
};
|
package/src/cli/utils/config.js
CHANGED
|
@@ -2,10 +2,36 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const getWorkspaceConfig = () => {
|
|
5
|
-
let
|
|
6
|
-
|
|
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
|
-
|
|
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 };
|
package/src/client/index.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
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
|
|
32
|
+
return GlobalStore.state[key];
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
subscribe(key, callback) {
|
|
26
|
-
if (!
|
|
27
|
-
|
|
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
|
-
|
|
43
|
+
GlobalStore.listeners[key] = GlobalStore.listeners[key].filter(cb => cb !== callback);
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
//
|
|
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
|
/**
|
package/src/plugin/index.js
CHANGED
|
@@ -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': {
|
|
100
|
-
|
|
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
|
})
|