@codemoreira/esad 1.0.1 → 1.1.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/README.md CHANGED
@@ -11,31 +11,31 @@ ESAD is a unified toolkit designed to abstract all the complexity from Super App
11
11
  ### 1. Initialize a Workspace
12
12
  Creates a main project folder with a Host (Expo-ready) and a global configuration.
13
13
  ```bash
14
- npx esad init nome-do-projeto
14
+ npx @codemoreira/esad init my-project
15
15
  ```
16
16
 
17
17
  ### 2. Create a Federated Module
18
18
  Scaffolds a new mini-app correctly named and configured to join the Super App.
19
19
  ```bash
20
- esad create-module modulo-rh
20
+ npx esad create-module module-rh
21
21
  ```
22
22
 
23
23
  ### 3. Create a Service Registry / CDN
24
24
  Sets up the backend registry used for dynamic routing and file hosting.
25
25
  ```bash
26
- esad create-cdn
26
+ npx esad create-cdn
27
27
  ```
28
28
 
29
29
  ### 4. Development Mode (Real-time HMR)
30
30
  Starts the local packager and **automatically** notifies the Registry to bypass the CDN, so your Host App sees your local changes instantly.
31
31
  ```bash
32
- esad dev --id modulo-rh --port 8081
32
+ npx esad dev --id module-rh --port 8081
33
33
  ```
34
34
 
35
35
  ### 5. Deployment
36
36
  Builds, zips, and uploads the module bundle to the configured CDN endpoint.
37
37
  ```bash
38
- esad deploy --id modulo-rh --version 1.0.0 --entry index.bundle
38
+ npx esad deploy --id module-rh --version 1.0.0 --entry index.bundle
39
39
  ```
40
40
 
41
41
  ---
@@ -49,7 +49,7 @@ import { withESAD } from 'esad/plugin';
49
49
 
50
50
  export default withESAD({
51
51
  type: 'module', // or 'host'
52
- id: 'meu-mini-app'
52
+ id: 'my-mini-app'
53
53
  });
54
54
  ```
55
55
 
@@ -63,17 +63,12 @@ const [token, setToken] = useESADState('auth_token');
63
63
 
64
64
  ---
65
65
 
66
- ## šŸ“¦ Publishing Updates
66
+ ## šŸ  Host App Features (by Default)
67
67
 
68
- Whenever you make changes to the **ESAD** source code (CLI or Lib), follow these steps to push a new version to NPM:
68
+ When you run `esad init`, the generated Host App comes pre-configured with:
69
69
 
70
- 1. **Bump the version**:
71
- Open `package.json` and increase the version number (e.g., `1.0.0` to `1.0.1`).
72
- *Tip: Use `npm version patch` in the terminal to do this automatically.*
73
-
74
- 2. **Publish**:
75
- ```bash
76
- npm publish
77
- ```
78
-
79
- Clients already using ESAD can then run `npm update esad` to get your latest improvements!
70
+ - **šŸŽØ NativeWind v4**: Utility-first styling ready to use with Tailwind CSS logic.
71
+ - **šŸ” Auth System**: Complete `AuthProvider` with persistent token storage using `expo-secure-store`.
72
+ - **šŸ›¤ļø Protected Routes**: Automatic redirection between `login` and `(protected)` groups based on auth state.
73
+ - **šŸ“¦ Module Loader**: A robust `lib/moduleLoader.ts` to fetch and initialize federated modules dynamically.
74
+ - **šŸ“± Premium UI**: A clean, modern starting point for your Super App dashboard.
package/bin/esad.js CHANGED
@@ -1,149 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
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');
4
+
5
+ // Import Commands
6
+ const initCommand = require('../src/cli/commands/init');
7
+ const createModuleCommand = require('../src/cli/commands/createModule');
8
+ const createCdnCommand = require('../src/cli/commands/createCdn');
9
+ const deployCommand = require('../src/cli/commands/deploy');
10
+ const devCommand = require('../src/cli/commands/dev');
8
11
 
9
12
  program
10
- .version('1.0.0')
13
+ .version('1.1.0')
11
14
  .description('esad - Easy Super App Development Toolkit');
12
15
 
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
16
  // --- COMMMAND: esad init ---
32
17
  program
33
18
  .command('init <project-name>')
34
19
  .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
- });
20
+ .action(initCommand);
95
21
 
96
22
  // --- COMMAND: esad create-cdn ---
97
23
  program
98
24
  .command('create-cdn [cdn-name]')
99
25
  .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
- });
26
+ .action(createCdnCommand);
111
27
 
112
28
  // --- COMMAND: esad create-module ---
113
29
  program
114
30
  .command('create-module <module-name>')
115
31
  .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
- });
32
+ .action(createModuleCommand);
147
33
 
148
34
  // --- COMMAND: esad deploy ---
149
35
  program
@@ -152,43 +38,7 @@ program
152
38
  .requiredOption('-i, --id <moduleId>', 'The Module ID to deploy')
153
39
  .requiredOption('-e, --entry <entryFileName>', 'The name of the main entry bundle (e.g., index.bundle)')
154
40
  .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
- });
41
+ .action(deployCommand);
192
42
 
193
43
  // --- COMMAND: esad dev ---
194
44
  program
@@ -196,38 +46,6 @@ program
196
46
  .requiredOption('-i, --id <moduleId>', 'The Module ID to run in dev mode')
197
47
  .option('-p, --port <port>', 'The port to run the dev server on', '8081')
198
48
  .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
- });
49
+ .action(devCommand);
232
50
 
233
51
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemoreira/esad",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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
  "exports": {
@@ -0,0 +1,13 @@
1
+ const { getWorkspaceConfig } = require('../utils/config');
2
+
3
+ module.exports = async (cdnName) => {
4
+ const configObj = getWorkspaceConfig();
5
+ if (!configObj) {
6
+ console.error(`āŒ Error: Call this command from inside an ESAD workspace (esad.config.json not found).`);
7
+ return;
8
+ }
9
+ const finalCdnName = cdnName || `${configObj.data.projectName}-cdn`;
10
+ console.log(`\nšŸ“¦ Creating CDN Registry: ${finalCdnName}...\n`);
11
+ // Placeholder for backend cloning
12
+ console.log(`[TODO] Scaffold Node Express backend into ./${finalCdnName}`);
13
+ };
@@ -0,0 +1,36 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { runProcess } = require('../utils/process');
4
+ const { getWorkspaceConfig } = require('../utils/config');
5
+
6
+ module.exports = async (moduleName) => {
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 { projectName } = configObj.data;
14
+ const isPrefixed = moduleName.startsWith(`${projectName}-`);
15
+ const finalModuleName = isPrefixed ? moduleName : `${projectName}-\${moduleName}`;
16
+
17
+ const workspaceDir = path.dirname(configObj.path);
18
+ const targetDir = path.join(workspaceDir, finalModuleName);
19
+
20
+ console.log(`\nšŸ“¦ Creating federated mini-app: ${finalModuleName}...\n`);
21
+
22
+ try {
23
+ await runProcess('npx', ['react-native@latest', 'init', finalModuleName], workspaceDir);
24
+ console.log(`\nšŸ“¦ Installing ESAD dependencies...`);
25
+ // Note: Assuming local link or npm install depends on final workflow
26
+ await runProcess('npm', ['install', '@codemoreira/esad'], targetDir);
27
+
28
+ const rspackContent = `import { withESAD } from '@codemoreira/esad/plugin';\n\nexport default withESAD({\n type: 'module',\n id: '${finalModuleName}'\n});\n`
29
+ fs.writeFileSync(path.join(targetDir, 'rspack.config.mjs'), rspackContent);
30
+ console.log(`āœ… Injected withESAD wrapper into rspack.config.mjs`);
31
+
32
+ console.log(`\nšŸŽ‰ Module ${finalModuleName} is ready!`);
33
+ } catch (err) {
34
+ console.error(`āŒ Failed to scaffold module`, err.message);
35
+ }
36
+ };
@@ -0,0 +1,42 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const AdmZip = require('adm-zip');
4
+ const { getWorkspaceConfig } = require('../utils/config');
5
+
6
+ module.exports = async (options) => {
7
+ console.log(`\nā˜ļø Starting ESAD Deploy for ${options.id} (v${options.version})\n`);
8
+
9
+ const configObj = getWorkspaceConfig();
10
+ if (!configObj) {
11
+ console.error(`āŒ Error: esad.config.json not found in current directory or parent.`);
12
+ process.exit(1);
13
+ }
14
+
15
+ const config = configObj.data;
16
+ if (!config.deployEndpoint) {
17
+ console.error(`āŒ Error: 'deployEndpoint' not configured in esad.config.json.`);
18
+ process.exit(1);
19
+ }
20
+
21
+ const deployUrl = config.deployEndpoint.replace('{{moduleId}}', options.id);
22
+ console.log(`šŸ“” Deployment Endpoint Resolved: ${deployUrl}`);
23
+
24
+ const distPath = path.join(process.cwd(), 'dist');
25
+ if (!fs.existsSync(distPath)) {
26
+ console.error(`āŒ Error: dist/ directory not found. Did you run the build command?`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const zip = new AdmZip();
31
+ zip.addLocalFolder(distPath);
32
+
33
+ const zipPath = path.join(process.cwd(), `bundle-${options.id}-${options.version}.zip`);
34
+ zip.writeZip(zipPath);
35
+ console.log(`šŸ—œļø Zipped output to ${zipPath}`);
36
+
37
+ console.log(`šŸš€ Uploading to CDN via multipart POST...`);
38
+ // Example fetch upload would go here
39
+
40
+ console.log(`āœ… [SIMULATED] Successfully uploaded to ${deployUrl}`);
41
+ fs.unlinkSync(zipPath);
42
+ };
@@ -0,0 +1,36 @@
1
+ const { spawn } = require('cross-spawn');
2
+ const { getWorkspaceConfig } = require('../utils/config');
3
+
4
+ module.exports = async (options) => {
5
+ console.log(`\n⚔ Starting ESAD Dev Server for ${options.id} on port ${options.port}...\n`);
6
+
7
+ const configObj = getWorkspaceConfig();
8
+ const config = configObj ? configObj.data : null;
9
+ let devApiUrl = config?.devModeEndpoint ? config.devModeEndpoint.replace('{{moduleId}}', options.id) : null;
10
+
11
+ const setDevMode = async (isActive) => {
12
+ if (!devApiUrl) return;
13
+ try {
14
+ const body = {
15
+ is_dev_mode: isActive,
16
+ ...(isActive && { dev_url: `http://localhost:${options.port}/index.bundle` })
17
+ };
18
+ const res = await fetch(devApiUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
19
+ if (res.ok) console.log(`šŸ“” Registry Sync: Dev Override is ${isActive ? 'ON' : 'OFF'} para o modulo Host`);
20
+ } catch(e) { /* ignore */ }
21
+ };
22
+
23
+ await setDevMode(true);
24
+
25
+ const proc = spawn('npx', ['react-native', 'webpack-start', '--port', options.port], { stdio: 'inherit', shell: true });
26
+
27
+ const shutdown = async () => {
28
+ console.log(`\nšŸ›‘ Parando ESAD Dev Server e revertendo o registro na CDN...`);
29
+ await setDevMode(false);
30
+ proc.kill();
31
+ process.exit(0);
32
+ };
33
+
34
+ process.on('SIGINT', shutdown);
35
+ process.on('SIGTERM', shutdown);
36
+ };
@@ -0,0 +1,88 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { runProcess } = require('../utils/process');
4
+ const templates = require('../templates/host');
5
+
6
+ module.exports = async (projectName) => {
7
+ const workspaceDir = path.join(process.cwd(), projectName);
8
+ console.log(`\nšŸš€ Initializing ESAD Workspace: ${projectName}...\n`);
9
+
10
+ fs.ensureDirSync(workspaceDir);
11
+
12
+ const configPath = path.join(workspaceDir, 'esad.config.json');
13
+ if (!fs.existsSync(configPath)) {
14
+ const configTemplate = {
15
+ projectName: projectName,
16
+ registryUrl: "http://localhost:3000/modules",
17
+ deployEndpoint: "http://localhost:3000/api/admin/modules/{{moduleId}}/versions",
18
+ devModeEndpoint: "http://localhost:3000/api/admin/modules/{{moduleId}}"
19
+ };
20
+ fs.writeJsonSync(configPath, configTemplate, { spaces: 2 });
21
+ console.log(`āœ… Generated dynamic configuration file: esad.config.json`);
22
+ }
23
+
24
+ const gitignorePath = path.join(workspaceDir, '.gitignore');
25
+ if (!fs.existsSync(gitignorePath)) {
26
+ const hostName = `${projectName}-host`;
27
+ const gitignoreContent = `# ESAD Workspace Git Configuration\n` +
28
+ `# Ignore everything by default\n` +
29
+ `/*\n\n` +
30
+ `# Exceptions: Track only the Host and Configs\n` +
31
+ `!/${hostName}/\n` +
32
+ `!/esad.config.json\n` +
33
+ `!/.gitignore\n` +
34
+ `\n# Ignore node_modules\n` +
35
+ `node_modules/\n`;
36
+ fs.writeFileSync(gitignorePath, gitignoreContent);
37
+ console.log(`āœ… Generated .gitignore`);
38
+ }
39
+
40
+ const hostName = `${projectName}-host`;
41
+ const hostDir = path.join(workspaceDir, hostName);
42
+
43
+ try {
44
+ console.log(`\nšŸ“¦ Scaffolding clean Expo project: ${hostName}...`);
45
+ await runProcess('npx', ['create-expo-app', hostName, '--template', 'blank'], workspaceDir);
46
+
47
+ console.log(`\nšŸ“¦ Installing ESAD and UI dependencies into host...`);
48
+ const deps = [
49
+ '@codemoreira/esad',
50
+ 'nativewind',
51
+ 'tailwindcss',
52
+ 'postcss',
53
+ 'autoprefixer',
54
+ 'expo-secure-store',
55
+ 'react-native-reanimated',
56
+ 'react-native-safe-area-context',
57
+ 'react-native-screens',
58
+ 'expo-router'
59
+ ];
60
+ await runProcess('npm', ['install', ...deps], hostDir);
61
+
62
+ console.log(`\nšŸŽØ Configuring NativeWind & Tailwind...`);
63
+ fs.writeFileSync(path.join(hostDir, 'tailwind.config.js'), templates.tailwindConfig);
64
+ fs.writeFileSync(path.join(hostDir, 'babel.config.js'), templates.babelConfig);
65
+
66
+ const rspackContent = `import { withESAD } from '@codemoreira/esad/plugin';\n\nexport default withESAD({\n type: 'host',\n id: '${hostName}'\n});\n`
67
+ fs.writeFileSync(path.join(hostDir, 'rspack.config.mjs'), rspackContent);
68
+
69
+ console.log(`\nšŸ” Scaffolding Auth & Navigation...`);
70
+ fs.ensureDirSync(path.join(hostDir, 'providers'));
71
+ fs.ensureDirSync(path.join(hostDir, 'hooks'));
72
+ fs.ensureDirSync(path.join(hostDir, 'lib'));
73
+ fs.ensureDirSync(path.join(hostDir, 'app', '(protected)', 'module'));
74
+
75
+ fs.writeFileSync(path.join(hostDir, 'providers/auth.tsx'), templates.authProvider);
76
+ fs.writeFileSync(path.join(hostDir, 'app/_layout.tsx'), templates.rootLayout);
77
+ fs.writeFileSync(path.join(hostDir, 'app/login.tsx'), templates.loginPage);
78
+ fs.writeFileSync(path.join(hostDir, 'app/global.css'), templates.globalCss);
79
+ fs.writeFileSync(path.join(hostDir, 'lib/moduleLoader.ts'), templates.moduleLoader);
80
+ fs.writeFileSync(path.join(hostDir, 'app/(protected)/index.tsx'), templates.dashboard);
81
+ fs.writeFileSync(path.join(hostDir, 'app/(protected)/module/[id].tsx'), templates.modulePage);
82
+
83
+ console.log(`\nšŸŽ‰ ESAD Workpace Initialized!`);
84
+ console.log(`-> cd ${projectName}\n-> esad dev (to start Host)`);
85
+ } catch (err) {
86
+ console.error(`āŒ Failed to init Host:`, err.message);
87
+ }
88
+ };
@@ -0,0 +1,199 @@
1
+ module.exports = {
2
+ tailwindConfig: `/** @type {import('tailwindcss').Config} */
3
+ module.exports = {
4
+ content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}", "./lib/**/*.{js,jsx,ts,tsx}"],
5
+ presets: [require("nativewind/preset")],
6
+ theme: {
7
+ extend: {},
8
+ },
9
+ plugins: [],
10
+ };`,
11
+
12
+ babelConfig: `module.exports = function (api) {
13
+ api.cache(true);
14
+ return {
15
+ presets: [
16
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
17
+ "nativewind/babel",
18
+ ],
19
+ };
20
+ };`,
21
+
22
+ authProvider: `import { createContext, useEffect, useState, useContext } from "react";
23
+ import * as SecureStore from "expo-secure-store";
24
+
25
+ const AuthContext = createContext({});
26
+
27
+ export function AuthProvider({ children }) {
28
+ const [token, setToken] = useState(null);
29
+ const [isInitialized, setIsInitialized] = useState(false);
30
+
31
+ useEffect(() => {
32
+ SecureStore.getItemAsync("token").then((val) => {
33
+ setToken(val);
34
+ setIsInitialized(true);
35
+ });
36
+ }, []);
37
+
38
+ const login = (mockToken = "session_token") => {
39
+ setToken(mockToken);
40
+ SecureStore.setItemAsync("token", mockToken);
41
+ };
42
+
43
+ const logout = () => {
44
+ setToken(null);
45
+ SecureStore.deleteItemAsync("token");
46
+ };
47
+
48
+ return (
49
+ <AuthContext.Provider value={{ token, login, logout, isInitialized }}>
50
+ {children}
51
+ </AuthContext.Provider>
52
+ );
53
+ }
54
+
55
+ export const useAuth = () => useContext(AuthContext);`,
56
+
57
+ rootLayout: `import { Stack, router } from "expo-router";
58
+ import { useEffect } from "react";
59
+ import { AuthProvider, useAuth } from "@/providers/auth";
60
+ import { SafeAreaProvider } from "react-native-safe-area-context";
61
+ import "./global.css";
62
+
63
+ export default function RootLayout() {
64
+ return (
65
+ <SafeAreaProvider>
66
+ <AuthProvider>
67
+ <RootNavigation />
68
+ </AuthProvider>\n </SafeAreaProvider>
69
+ );
70
+ }
71
+
72
+ function RootNavigation() {
73
+ const { token, isInitialized } = useAuth();
74
+
75
+ useEffect(() => {
76
+ if (!isInitialized) return;\n\n // Global SuperApp Registry
77
+ globalThis.__SUPERAPP__ = {
78
+ getToken: () => token,
79
+ navigate: (route, params) => router.push({ pathname: route, params })
80
+ };\n }, [isInitialized, token]);
81
+
82
+ if (!isInitialized) return null;
83
+
84
+ return (
85
+ <Stack screenOptions={{ headerShown: false }}>
86
+ <Stack.Screen name="login" redirect={!!token} />
87
+ <Stack.Screen name="(protected)" redirect={!token} />
88
+ </Stack>
89
+ );
90
+ }`,
91
+
92
+ loginPage: `import { View, Text, TouchableOpacity } from "react-native";
93
+ import { useAuth } from "@/providers/auth";
94
+
95
+ export default function LoginPage() {
96
+ const { login } = useAuth();
97
+
98
+ return (
99
+ <View className="flex-1 items-center justify-center bg-slate-50 p-6">
100
+ <View className="w-full max-w-sm bg-white p-8 rounded-3xl shadow-xl border border-slate-100">
101
+ <Text className="text-3xl font-bold text-slate-900 mb-2">Welcome</Text>
102
+ <Text className="text-slate-500 mb-8">Sign in to access your SuperApp modules</Text>\n
103
+ <TouchableOpacity
104
+ onPress={() => login()}
105
+ className="w-full bg-indigo-600 py-4 rounded-2xl items-center shadow-lg active:bg-indigo-700"
106
+ >
107
+ <Text className="text-white font-semibold text-lg">Entrar no App</Text>
108
+ </TouchableOpacity>
109
+ </View>
110
+ </View>
111
+ );
112
+ }`,
113
+
114
+ globalCss: `@tailwind base;\n@tailwind components;\n@tailwind utilities;`,
115
+
116
+ moduleLoader: `import { ScriptManager } from "@callstack/repack/client";
117
+
118
+ export async function loadModule(config) {
119
+ await ScriptManager.shared.addScript({
120
+ id: config.id,
121
+ url: config.url
122
+ });
123
+
124
+ const container = global[config.scope];
125
+ if (!container) throw new Error("Module " + config.scope + " not found");
126
+
127
+ await container.init(__webpack_share_scopes__.default);
128
+ const factory = await container.get(config.module);
129
+ return factory();
130
+ }`,
131
+
132
+ dashboard: `import { View, Text, ScrollView, TouchableOpacity } from "react-native";
133
+ import { Link } from "expo-router";
134
+ import { useAuth } from "@/providers/auth";
135
+
136
+ const MOCK_MODULES = [
137
+ { id: 'mod1', name: 'Sales Dashboard', icon: 'šŸ“Š' },
138
+ { id: 'mod2', name: 'Inventory Manager', icon: 'šŸ“¦' },
139
+ ];
140
+
141
+ export default function Dashboard() {
142
+ const { logout } = useAuth();
143
+
144
+ return (
145
+ <ScrollView className="flex-1 bg-slate-50 p-6 pt-16">
146
+ <View className="flex-row justify-between items-center mb-8">
147
+ <View>
148
+ <Text className="text-sm text-slate-500">Welcome back,</Text>
149
+ <Text className="text-2xl font-bold text-slate-900">Explorer</Text>
150
+ </View>
151
+ <TouchableOpacity onPress={logout} className="p-2">
152
+ <Text className="text-red-500 font-medium">Logout</Text>
153
+ </TouchableOpacity>
154
+ </View>
155
+
156
+ <Text className="text-lg font-semibold text-slate-800 mb-4">Your Modules</Text>
157
+
158
+ {MOCK_MODULES.map(m => (
159
+ <Link key={m.id} href={\`/(protected)/module/${m.id}\`} asChild>
160
+ <TouchableOpacity className="bg-white p-6 rounded-2xl mb-4 shadow-sm border border-slate-100 flex-row items-center">
161
+ <Text className="text-3xl mr-4">{m.icon}</Text>
162
+ <View className="flex-1">
163
+ <Text className="text-lg font-bold text-slate-900">{m.name}</Text>
164
+ <Text className="text-slate-500">Tap to open module</Text>
165
+ </View>
166
+ </TouchableOpacity>
167
+ </Link>
168
+ ))}
169
+ </ScrollView>
170
+ );
171
+ }`,
172
+
173
+ modulePage: `import { useLocalSearchParams } from "expo-router";
174
+ import { View, Text, ActivityIndicator } from "react-native";
175
+ import { useEffect, useState } from "react";
176
+ import { loadModule } from "@/lib/moduleLoader";
177
+
178
+ export default function ModulePage() {
179
+ const { id } = useLocalSearchParams();
180
+ const [Module, setModule] = useState(null);
181
+ const [error, setError] = useState(null);
182
+
183
+ useEffect(() => {\n // In real app, fetch config from registry by id\n setError("Module dynamic loading requires a running CDN and Build setup.");\n }, [id]);
184
+
185
+ if (error) return (
186
+ <View className="flex-1 items-center justify-center p-10 bg-white">
187
+ <Text className="text-red-500 text-center font-medium mb-4">{error}</Text>
188
+ <Text className="text-slate-400 text-center text-sm">Follow the README to setup your module registry.</Text>
189
+ </View>
190
+ );
191
+
192
+ return (
193
+ <View className="flex-1 bg-white items-center justify-center">
194
+ <ActivityIndicator size="large" color="#4f46e5" />
195
+ <Text className="mt-4 text-slate-500">Loading federated module...</Text>
196
+ </View>
197
+ );
198
+ }`
199
+ };
@@ -0,0 +1,11 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ 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) };
9
+ };
10
+
11
+ module.exports = { getWorkspaceConfig };
@@ -0,0 +1,20 @@
1
+ const { spawn } = require('cross-spawn');
2
+
3
+ /**
4
+ * Helper to spawn commands synchronously
5
+ * @param {string} cmd
6
+ * @param {string[]} args
7
+ * @param {string} cwd
8
+ * @returns {Promise<void>}
9
+ */
10
+ const runProcess = (cmd, args, cwd = process.cwd()) => {
11
+ return new Promise((resolve, reject) => {
12
+ const child = spawn(cmd, args, { stdio: 'inherit', cwd, shell: true });
13
+ child.on('close', code => {
14
+ if (code !== 0) reject(new Error(`Command ${cmd} ${args.join(' ')} failed`));
15
+ else resolve();
16
+ });
17
+ });
18
+ };
19
+
20
+ module.exports = { runProcess };