@codemoreira/esad 1.0.0

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 ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const { spawn } = require('cross-spawn');
7
+ const AdmZip = require('adm-zip');
8
+
9
+ program
10
+ .version('1.0.0')
11
+ .description('esad - Easy Super App Development Toolkit');
12
+
13
+ // Helper to spawn commands synchronously
14
+ const runProcess = (cmd, args, cwd = process.cwd()) => {
15
+ return new Promise((resolve, reject) => {
16
+ const child = spawn(cmd, args, { stdio: 'inherit', cwd, shell: true });
17
+ child.on('close', code => {
18
+ if (code !== 0) reject(new Error(`Command ${cmd} ${args.join(' ')} failed`));
19
+ else resolve();
20
+ });
21
+ });
22
+ };
23
+
24
+ const getWorkspaceConfig = () => {
25
+ let configPath = path.join(process.cwd(), 'esad.config.json');
26
+ if (!fs.existsSync(configPath)) configPath = path.join(process.cwd(), '../esad.config.json');
27
+ if (!fs.existsSync(configPath)) return null;
28
+ return { path: configPath, data: fs.readJsonSync(configPath) };
29
+ };
30
+
31
+ // --- COMMMAND: esad init ---
32
+ program
33
+ .command('init <project-name>')
34
+ .description('Scaffold a new ESAD workspace containing the Host App')
35
+ .action(async (projectName) => {
36
+ const workspaceDir = path.join(process.cwd(), projectName);
37
+ console.log(`\nšŸš€ Initializing ESAD Workspace: ${projectName}...\n`);
38
+
39
+ // Create Workspace Dir
40
+ fs.ensureDirSync(workspaceDir);
41
+
42
+ // Create base esad.config.json at root workspace
43
+ const configPath = path.join(workspaceDir, 'esad.config.json');
44
+ if (!fs.existsSync(configPath)) {
45
+ const configTemplate = {
46
+ projectName: projectName,
47
+ registryUrl: "http://localhost:3000/modules",
48
+ deployEndpoint: "http://localhost:3000/api/admin/modules/{{moduleId}}/versions",
49
+ devModeEndpoint: "http://localhost:3000/api/admin/modules/{{moduleId}}"
50
+ };
51
+ fs.writeJsonSync(configPath, configTemplate, { spaces: 2 });
52
+ console.log(`āœ… Generated dynamic configuration file: esad.config.json`);
53
+ }
54
+
55
+ // Create .gitignore to allow only host and config (Poly-repo support)
56
+ const gitignorePath = path.join(workspaceDir, '.gitignore');
57
+ if (!fs.existsSync(gitignorePath)) {
58
+ const hostName = `${projectName}-host`;
59
+ const gitignoreContent = `# ESAD Workspace Git Configuration\n` +
60
+ `# Ignore everything by default\n` +
61
+ `/*\n\n` +
62
+ `# Exceptions: Track only the Host and Configs\n` +
63
+ `!/${hostName}/\n` +
64
+ `!/esad.config.json\n` +
65
+ `!/.gitignore\n` +
66
+ `\n# Ignore node_modules\n` +
67
+ `node_modules/\n`;
68
+ fs.writeFileSync(gitignorePath, gitignoreContent);
69
+ console.log(`āœ… Generated .gitignore (Whitelist mode: ignored modules so they can have their own repos)`);
70
+ }
71
+
72
+ // Scaffold Expo app using create-expo-app
73
+ const hostName = `${projectName}-host`;
74
+ const hostDir = path.join(workspaceDir, hostName);
75
+
76
+ try {
77
+ console.log(`\nšŸ“¦ Scaffolding clean Expo project: ${hostName}...`);
78
+ await runProcess('npx', ['create-expo-app', hostName, '--template', 'blank'], workspaceDir);
79
+
80
+ console.log(`\nšŸ“¦ Installing ESAD dependencies into host...`);
81
+ // Simulate local ESAD package link (in reality it would be an npm publish fetch)
82
+ await runProcess('npm', ['install', '../../esad'], hostDir);
83
+
84
+ // Generate the Host's rspack config using withESAD
85
+ const rspackContent = `import { withESAD } from 'esad/plugin';\n\nexport default withESAD({\n type: 'host',\n id: '${hostName}'\n});\n`;
86
+ fs.writeFileSync(path.join(hostDir, 'rspack.config.mjs'), rspackContent);
87
+ console.log(`āœ… Injected withESAD wrapper into rspack.config.mjs`);
88
+
89
+ console.log(`\nšŸŽ‰ ESAD Workpace Initialized!`);
90
+ console.log(`-> cd ${projectName}\n-> esad create-cdn\n-> esad create-module modulo1`);
91
+ } catch (err) {
92
+ console.error(`āŒ Failed to init Host:`, err.message);
93
+ }
94
+ });
95
+
96
+ // --- COMMAND: esad create-cdn ---
97
+ program
98
+ .command('create-cdn [cdn-name]')
99
+ .description('Scaffold the CDN / Registry backend')
100
+ .action(async (cdnName) => {
101
+ const configObj = getWorkspaceConfig();
102
+ if (!configObj) {
103
+ console.error(`āŒ Error: Call this command from inside an ESAD workspace (esad.config.json not found).`);
104
+ return;
105
+ }
106
+ const finalCdnName = cdnName || `${configObj.data.projectName}-cdn`;
107
+ console.log(`\nšŸ“¦ Creating CDN Registry: ${finalCdnName}...\n`);
108
+ // Placeholder for backend cloning
109
+ console.log(`[TODO] Scaffold Node Express backend into ./${finalCdnName}`);
110
+ });
111
+
112
+ // --- COMMAND: esad create-module ---
113
+ program
114
+ .command('create-module <module-name>')
115
+ .description('Scaffold a React Native mini-app automatically configured for Module Federation via ESAD')
116
+ .action(async (moduleName) => {
117
+ const configObj = getWorkspaceConfig();
118
+ if (!configObj) {
119
+ console.error(`āŒ Error: Call this command from inside an ESAD workspace (esad.config.json not found).`);
120
+ return;
121
+ }
122
+
123
+ const { projectName } = configObj.data;
124
+ const isPrefixed = moduleName.startsWith(`${projectName}-`);
125
+ const finalModuleName = isPrefixed ? moduleName : `${projectName}-${moduleName}`;
126
+
127
+ const workspaceDir = path.dirname(configObj.path);
128
+ const targetDir = path.join(workspaceDir, finalModuleName);
129
+
130
+ console.log(`\nšŸ“¦ Creating federated mini-app: ${finalModuleName}...\n`);
131
+
132
+ try {
133
+ await runProcess('npx', ['react-native@latest', 'init', finalModuleName], workspaceDir);
134
+ console.log(`\nšŸ“¦ Installing ESAD dependencies...`);
135
+ // Simulate local link
136
+ await runProcess('npm', ['install', '../../esad'], targetDir);
137
+
138
+ const rspackContent = `import { withESAD } from 'esad/plugin';\n\nexport default withESAD({\n type: 'module',\n id: '${finalModuleName}'\n});\n`;
139
+ fs.writeFileSync(path.join(targetDir, 'rspack.config.mjs'), rspackContent);
140
+ console.log(`āœ… Injected withESAD wrapper into rspack.config.mjs`);
141
+
142
+ console.log(`\nšŸŽ‰ Module ${finalModuleName} is ready!`);
143
+ } catch (err) {
144
+ console.error(`āŒ Failed to scaffold module`, err.message);
145
+ }
146
+ });
147
+
148
+ // --- COMMAND: esad deploy ---
149
+ program
150
+ .command('deploy')
151
+ .requiredOption('-v, --version <semver>', 'Version number (e.g., 1.0.0)')
152
+ .requiredOption('-i, --id <moduleId>', 'The Module ID to deploy')
153
+ .requiredOption('-e, --entry <entryFileName>', 'The name of the main entry bundle (e.g., index.bundle)')
154
+ .description('Zips the local dist directory and uploads it to the configured deployment endpoint')
155
+ .action(async (options) => {
156
+ console.log(`\nā˜ļø Starting ESAD Deploy for ${options.id} (v${options.version})\n`);
157
+
158
+ const configObj = getWorkspaceConfig();
159
+ if (!configObj) {
160
+ console.error(`āŒ Error: esad.config.json not found in current directory or parent.`);
161
+ process.exit(1);
162
+ }
163
+
164
+ const config = configObj.data;
165
+ if (!config.deployEndpoint) {
166
+ console.error(`āŒ Error: 'deployEndpoint' not configured in esad.config.json.`);
167
+ process.exit(1);
168
+ }
169
+
170
+ const deployUrl = config.deployEndpoint.replace('{{moduleId}}', options.id);
171
+ console.log(`šŸ“” Deployment Endpoint Resolved: ${deployUrl}`);
172
+
173
+ const distPath = path.join(process.cwd(), 'dist');
174
+ if (!fs.existsSync(distPath)) {
175
+ console.error(`āŒ Error: dist/ directory not found. Did you run the build command?`);
176
+ process.exit(1);
177
+ }
178
+
179
+ const zip = new AdmZip();
180
+ zip.addLocalFolder(distPath);
181
+
182
+ const zipPath = path.join(process.cwd(), `bundle-${options.id}-${options.version}.zip`);
183
+ zip.writeZip(zipPath);
184
+ console.log(`šŸ—œļø Zipped output to ${zipPath}`);
185
+
186
+ console.log(`šŸš€ Uploading to CDN via multipart POST...`);
187
+ // Example fetch upload would go here
188
+
189
+ console.log(`āœ… [SIMULATED] Successfully uploaded to ${deployUrl}`);
190
+ fs.unlinkSync(zipPath);
191
+ });
192
+
193
+ // --- COMMAND: esad dev ---
194
+ program
195
+ .command('dev')
196
+ .requiredOption('-i, --id <moduleId>', 'The Module ID to run in dev mode')
197
+ .option('-p, --port <port>', 'The port to run the dev server on', '8081')
198
+ .description('Starts the dev server and updates the external registry to bypass CDN')
199
+ .action(async (options) => {
200
+ console.log(`\n⚔ Starting ESAD Dev Server for ${options.id} on port ${options.port}...\n`);
201
+
202
+ const configObj = getWorkspaceConfig();
203
+ const config = configObj ? configObj.data : null;
204
+ let devApiUrl = config?.devModeEndpoint ? config.devModeEndpoint.replace('{{moduleId}}', options.id) : null;
205
+
206
+ const setDevMode = async (isActive) => {
207
+ if (!devApiUrl) return;
208
+ try {
209
+ const body = {
210
+ is_dev_mode: isActive,
211
+ ...(isActive && { dev_url: `http://localhost:${options.port}/index.bundle` })
212
+ };
213
+ const res = await fetch(devApiUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
214
+ if (res.ok) console.log(`šŸ“” Registry Sync: Dev Override is ${isActive ? 'ON' : 'OFF'} para o modulo Host`);
215
+ } catch(e) { /* ignore */ }
216
+ };
217
+
218
+ await setDevMode(true);
219
+
220
+ const proc = spawn('npx', ['react-native', 'webpack-start', '--port', options.port], { stdio: 'inherit', shell: true });
221
+
222
+ const shutdown = async () => {
223
+ console.log(`\nšŸ›‘ Parando ESAD Dev Server e revertendo o registro na CDN...`);
224
+ await setDevMode(false);
225
+ proc.kill();
226
+ process.exit(0);
227
+ };
228
+
229
+ process.on('SIGINT', shutdown);
230
+ process.on('SIGTERM', shutdown);
231
+ });
232
+
233
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@codemoreira/esad",
3
+ "version": "1.0.0",
4
+ "description": "Easy Super App Development - Zero-Config CLI and DevTools for React Native Module Federation",
5
+ "main": "src/plugin/index.js",
6
+ "exports": {
7
+ ".": "./src/plugin/index.js",
8
+ "./plugin": "./src/plugin/index.js",
9
+ "./client": "./src/client/index.js"
10
+ },
11
+ "bin": {
12
+ "esad": "./bin/esad.js"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "src",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "test": "echo \"Error: no test specified\" && exit 1"
21
+ },
22
+ "dependencies": {
23
+ "adm-zip": "^0.5.10",
24
+ "chalk": "^4.1.2",
25
+ "commander": "^11.1.0",
26
+ "cross-spawn": "^7.0.3",
27
+ "fs-extra": "^11.2.0"
28
+ },
29
+ "devDependencies": {}
30
+ }
@@ -0,0 +1,69 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ /**
4
+ * ESAD Global Event Manager
5
+ * This class runs as a true Singleton across the Host and all Federated Modules,
6
+ * allowing instant variable sharing without tight coupling.
7
+ */
8
+ class ESADEventEmitter {
9
+ constructor() {
10
+ this.state = {};
11
+ this.listeners = {};
12
+ }
13
+
14
+ set(key, value) {
15
+ this.state[key] = value;
16
+ if (this.listeners[key]) {
17
+ this.listeners[key].forEach(callback => callback(value));
18
+ }
19
+ }
20
+
21
+ get(key) {
22
+ return this.state[key];
23
+ }
24
+
25
+ subscribe(key, callback) {
26
+ if (!this.listeners[key]) this.listeners[key] = [];
27
+ this.listeners[key].push(callback);
28
+ return () => {
29
+ this.listeners[key] = this.listeners[key].filter(cb => cb !== callback);
30
+ };
31
+ }
32
+ }
33
+
34
+ // Because this package is marked as a ModuleFederation Singleton,
35
+ // this instance will be shared identically across all chunks!
36
+ const ESADState = new ESADEventEmitter();
37
+
38
+ /**
39
+ * React Hook for subscribing to Global State Changes
40
+ * @param {string} key Unique identifier for the state slice (e.g. 'auth_token', 'theme')
41
+ * @param {any} initialValue Optional initial state fallback
42
+ */
43
+ export function useESADState(key, initialValue) {
44
+ const [val, setVal] = useState(() => {
45
+ const existing = ESADState.get(key);
46
+ if (existing !== undefined) return existing;
47
+ if (initialValue !== undefined) {
48
+ ESADState.set(key, initialValue);
49
+ return initialValue;
50
+ }
51
+ return undefined;
52
+ });
53
+
54
+ useEffect(() => {
55
+ // Whenever ESADState.set is called matching this key, this component will re-render
56
+ const unsubscribe = ESADState.subscribe(key, (newVal) => {
57
+ setVal(newVal);
58
+ });
59
+ return unsubscribe;
60
+ }, [key]);
61
+
62
+ const setter = (newVal) => {
63
+ ESADState.set(key, newVal);
64
+ };
65
+
66
+ return [val, setter];
67
+ }
68
+
69
+ export { ESADState };
@@ -0,0 +1,41 @@
1
+ const Repack = require('@callstack/repack');
2
+
3
+ /**
4
+ * ESAD Re.Pack Plugin Wrapper
5
+ * Abstracts away the boilerplate of Module Federation for SuperApps.
6
+ *
7
+ * @param {Object} options
8
+ * @param {string} options.type 'host' | 'module'
9
+ * @param {string} options.id Unique module or host ID
10
+ */
11
+ function withESAD(options) {
12
+ return (env) => {
13
+ // In a real scenario, we merge heavily with Repack.getTemplateConfig here
14
+ console.log(`[ESAD Plugin] Applying Zero-Config Re.Pack profile for ${options.type.toUpperCase()}: ${options.id}`);
15
+
16
+ // Ensure the esad state library is ALWAYS shared
17
+ const sharedConfig = {
18
+ react: { singleton: true, eager: options.type === 'host' },
19
+ 'react-native': { singleton: true, eager: options.type === 'host' },
20
+ 'esad/client': { singleton: true, eager: true } // Crucial for Global State
21
+ };
22
+
23
+ return {
24
+ // Configuration abstraction
25
+ plugins: [
26
+ new Repack.plugins.ModuleFederationPlugin({
27
+ name: options.id.replace(/-/g, '_'),
28
+ shared: sharedConfig,
29
+ // If type is module, also configure exposes
30
+ ...(options.type === 'module' && {
31
+ exposes: {
32
+ './App': './src/App'
33
+ }
34
+ })
35
+ })
36
+ ]
37
+ };
38
+ };
39
+ }
40
+
41
+ module.exports = { withESAD };