@codemoreira/esad 1.2.7 ā 1.3.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/package.json +44 -44
- package/src/cli/commands/createModule.js +9 -18
- package/src/cli/commands/dev.js +4 -0
- package/src/cli/commands/host.js +3 -41
- package/src/cli/commands/init.js +10 -64
- package/src/cli/templates/templates.json +4 -0
- package/src/cli/utils/scaffold.js +84 -0
- package/src/plugin/index.js +128 -129
- package/src/cli/templates/host.js +0 -201
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@codemoreira/esad",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Easy Super App Development - Zero-Config CLI and DevTools for React Native Module Federation",
|
|
5
|
-
"main": "src/plugin/index.js",
|
|
6
|
-
"types": "./src/plugin/index.d.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"types": "./src/plugin/index.d.ts",
|
|
10
|
-
"default": "./src/plugin/index.js"
|
|
11
|
-
},
|
|
12
|
-
"./plugin": {
|
|
13
|
-
"types": "./src/plugin/index.d.ts",
|
|
14
|
-
"default": "./src/plugin/index.js"
|
|
15
|
-
},
|
|
16
|
-
"./client": {
|
|
17
|
-
"types": "./src/client/index.d.ts",
|
|
18
|
-
"default": "./src/client/index.js"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"bin": {
|
|
22
|
-
"esad": "./bin/esad.js"
|
|
23
|
-
},
|
|
24
|
-
"files": [
|
|
25
|
-
"bin",
|
|
26
|
-
"src",
|
|
27
|
-
"README.md"
|
|
28
|
-
],
|
|
29
|
-
"scripts": {
|
|
30
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"adm-zip": "^0.5.10",
|
|
34
|
-
"chalk": "^4.1.2",
|
|
35
|
-
"commander": "^11.1.0",
|
|
36
|
-
"cross-spawn": "^7.0.3",
|
|
37
|
-
"fs-extra": "^11.2.0"
|
|
38
|
-
},
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@rspack/core": "^1.7.9",
|
|
41
|
-
"@types/react": "^19.2.14",
|
|
42
|
-
"@types/react-native": "^0.72.8"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@codemoreira/esad",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Easy Super App Development - Zero-Config CLI and DevTools for React Native Module Federation",
|
|
5
|
+
"main": "src/plugin/index.js",
|
|
6
|
+
"types": "./src/plugin/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/plugin/index.d.ts",
|
|
10
|
+
"default": "./src/plugin/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./plugin": {
|
|
13
|
+
"types": "./src/plugin/index.d.ts",
|
|
14
|
+
"default": "./src/plugin/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./client": {
|
|
17
|
+
"types": "./src/client/index.d.ts",
|
|
18
|
+
"default": "./src/client/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"esad": "./bin/esad.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"adm-zip": "^0.5.10",
|
|
34
|
+
"chalk": "^4.1.2",
|
|
35
|
+
"commander": "^11.1.0",
|
|
36
|
+
"cross-spawn": "^7.0.3",
|
|
37
|
+
"fs-extra": "^11.2.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@rspack/core": "^1.7.9",
|
|
41
|
+
"@types/react": "^19.2.14",
|
|
42
|
+
"@types/react-native": "^0.72.8"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -2,6 +2,8 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { runProcess } = require('../utils/process');
|
|
4
4
|
const { getWorkspaceConfig } = require('../utils/config');
|
|
5
|
+
const { cloneTemplate, renameProject } = require('../utils/scaffold');
|
|
6
|
+
const templatesConfig = require('../templates/templates.json');
|
|
5
7
|
|
|
6
8
|
module.exports = async (moduleName) => {
|
|
7
9
|
const configObj = getWorkspaceConfig();
|
|
@@ -20,25 +22,14 @@ module.exports = async (moduleName) => {
|
|
|
20
22
|
console.log(`\nš¦ Creating federated mini-app: ${finalModuleName}...\n`);
|
|
21
23
|
|
|
22
24
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.log(`ā
Injected withESAD wrapper into rspack.config.mjs`);
|
|
31
|
-
|
|
32
|
-
// Update package.json scripts
|
|
33
|
-
const modPkgPath = path.join(targetDir, 'package.json');
|
|
34
|
-
const modPkg = fs.readJsonSync(modPkgPath);
|
|
35
|
-
modPkg.scripts = {
|
|
36
|
-
...modPkg.scripts,
|
|
37
|
-
"start": "esad dev",
|
|
38
|
-
"deploy": "esad deploy"
|
|
39
|
-
};
|
|
40
|
-
fs.writeJsonSync(modPkgPath, modPkg, { spaces: 2 });
|
|
41
|
-
console.log(`ā
Abstracted module scripts to use ESAD CLI.`);
|
|
31
|
+
console.log(`\nš¦ Installing dependencies...`);
|
|
32
|
+
await runProcess('npm', ['install'], targetDir);
|
|
42
33
|
|
|
43
34
|
console.log(`\nš Module ${finalModuleName} is ready!`);
|
|
44
35
|
} catch (err) {
|
package/src/cli/commands/dev.js
CHANGED
|
@@ -2,6 +2,7 @@ const { spawn } = require('cross-spawn');
|
|
|
2
2
|
const { getWorkspaceConfig } = require('../utils/config');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { prepareNative } = require('../utils/scaffold');
|
|
5
6
|
|
|
6
7
|
module.exports = async (options) => {
|
|
7
8
|
let cwd = process.cwd();
|
|
@@ -39,6 +40,9 @@ module.exports = async (options) => {
|
|
|
39
40
|
// Determine if it's a Host or Module
|
|
40
41
|
const isHost = pkg.name.endsWith('-host') || pkg.dependencies?.['@callstack/repack'];
|
|
41
42
|
|
|
43
|
+
// 1. Initial Checks & Automated Native Preparation
|
|
44
|
+
await prepareNative(cwd, 'all');
|
|
45
|
+
|
|
42
46
|
if (isHost && !options.id) {
|
|
43
47
|
console.log(`\nš Starting Host App Dev Server (Re.Pack/Rspack)...\n`);
|
|
44
48
|
await spawn('npx', ['react-native', 'webpack-start'], { cwd, stdio: 'inherit', shell: true });
|
package/src/cli/commands/host.js
CHANGED
|
@@ -5,6 +5,7 @@ const { spawn } = require('cross-spawn');
|
|
|
5
5
|
const http = require('http');
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { getWorkspaceConfig } = require('../utils/config');
|
|
8
|
+
const { prepareNative } = require('../utils/scaffold');
|
|
8
9
|
|
|
9
10
|
const rl = readline.createInterface({
|
|
10
11
|
input: process.stdin,
|
|
@@ -38,48 +39,9 @@ module.exports = async (subcommand) => {
|
|
|
38
39
|
|
|
39
40
|
const pkg = fs.readJsonSync(pkgPath);
|
|
40
41
|
|
|
41
|
-
// 1. Initial Checks &
|
|
42
|
+
// 1. Initial Checks & Automated Native Preparation
|
|
42
43
|
if (subcommand === 'dev' || subcommand === 'start') {
|
|
43
|
-
|
|
44
|
-
console.log(`š¦ Native folders not found. Running expo prebuild...`);
|
|
45
|
-
await runProcess('npx', ['expo', 'prebuild'], cwd);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 2. Patch Native Files
|
|
49
|
-
console.log(`š§ Patching native files for Re.Pack compatibility...`);
|
|
50
|
-
const patchFiles = async () => {
|
|
51
|
-
// Android
|
|
52
|
-
const androidMainApp = path.join(cwd, 'android/app/src/main/java');
|
|
53
|
-
if (fs.existsSync(androidMainApp)) {
|
|
54
|
-
const files = await fs.readdir(androidMainApp, { recursive: true });
|
|
55
|
-
for (const file of files) {
|
|
56
|
-
if (file.endsWith('MainApplication.kt') || file.endsWith('MainApplication.java')) {
|
|
57
|
-
const filePath = path.join(androidMainApp, file);
|
|
58
|
-
let content = await fs.readFile(filePath, 'utf8');
|
|
59
|
-
if (content.includes('.expo/.virtual-metro-entry')) {
|
|
60
|
-
content = content.replace(/.expo\/.virtual-metro-entry/g, 'index');
|
|
61
|
-
await fs.writeFile(filePath, content);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// iOS
|
|
67
|
-
const iosDir = path.join(cwd, 'ios');
|
|
68
|
-
if (fs.existsSync(iosDir)) {
|
|
69
|
-
const iosFiles = await fs.readdir(iosDir, { recursive: true });
|
|
70
|
-
for (const file of iosFiles) {
|
|
71
|
-
if (file.match(/AppDelegate\.(m|mm|swift)/)) {
|
|
72
|
-
const filePath = path.join(iosDir, file);
|
|
73
|
-
let content = await fs.readFile(filePath, 'utf8');
|
|
74
|
-
if (content.includes('.expo/.virtual-metro-entry')) {
|
|
75
|
-
content = content.replace(/.expo\/.virtual-metro-entry/g, 'index');
|
|
76
|
-
await fs.writeFile(filePath, content);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
await patchFiles();
|
|
44
|
+
await prepareNative(cwd, 'all');
|
|
83
45
|
|
|
84
46
|
// 3. Platform Selection
|
|
85
47
|
console.log(`\nESAD Host Dev Manager`);
|
package/src/cli/commands/init.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { runProcess } = require('../utils/process');
|
|
4
|
-
const
|
|
4
|
+
const { cloneTemplate, renameProject } = require('../utils/scaffold');
|
|
5
|
+
const templatesConfig = require('../templates/templates.json');
|
|
5
6
|
|
|
6
7
|
module.exports = async (projectName) => {
|
|
7
8
|
const workspaceDir = path.join(process.cwd(), projectName);
|
|
@@ -41,72 +42,17 @@ module.exports = async (projectName) => {
|
|
|
41
42
|
const hostDir = path.join(workspaceDir, hostName);
|
|
42
43
|
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
-
await
|
|
45
|
+
// 1. Clone Template instead of create-expo-app
|
|
46
|
+
await cloneTemplate(templatesConfig.host, hostDir);
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const hostPkg = fs.readJsonSync(hostPkgPath);
|
|
50
|
-
const reactVersion = hostPkg.dependencies.react;
|
|
48
|
+
// 2. Rename Project
|
|
49
|
+
await renameProject(hostDir, hostName);
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'@callstack/repack@^5.2.5',
|
|
55
|
-
'@rspack/core@^1.7.9',
|
|
56
|
-
'@rspack/plugin-react-refresh@^1.6.1',
|
|
57
|
-
'@callstack/repack-plugin-expo-modules',
|
|
58
|
-
'@react-native-community/cli',
|
|
59
|
-
'nativewind',
|
|
60
|
-
'tailwindcss',
|
|
61
|
-
'postcss',
|
|
62
|
-
'autoprefixer',
|
|
63
|
-
'expo-secure-store',
|
|
64
|
-
'react-native-reanimated',
|
|
65
|
-
'react-native-safe-area-context',
|
|
66
|
-
'react-native-screens',
|
|
67
|
-
'expo-router',
|
|
68
|
-
`react-dom@${reactVersion}`
|
|
69
|
-
];
|
|
70
|
-
await runProcess('npm', ['install', ...deps], hostDir);
|
|
51
|
+
console.log(`\nš¦ Installing dependencies into host...`);
|
|
52
|
+
await runProcess('npm', ['install'], hostDir);
|
|
71
53
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Update package.json scripts to delegate to ESAD CLI
|
|
76
|
-
updatedPkg.scripts = {
|
|
77
|
-
...updatedPkg.scripts,
|
|
78
|
-
"start": "esad host start",
|
|
79
|
-
"android": "esad host android",
|
|
80
|
-
"ios": "esad host ios",
|
|
81
|
-
"dev": "esad host dev"
|
|
82
|
-
};
|
|
83
|
-
fs.writeJsonSync(hostPkgPath, updatedPkg, { spaces: 2 });
|
|
84
|
-
console.log(`ā
Abstracted package.json scripts to use ESAD CLI.`);
|
|
85
|
-
|
|
86
|
-
console.log(`\nšØ Configuring NativeWind & Tailwind...`);
|
|
87
|
-
fs.writeFileSync(path.join(hostDir, 'tailwind.config.js'), templates.tailwindConfig);
|
|
88
|
-
fs.writeFileSync(path.join(hostDir, 'babel.config.js'), templates.babelConfig);
|
|
89
|
-
|
|
90
|
-
const rspackContent = `import { withESAD } from '@codemoreira/esad/plugin';\n\nexport default withESAD({\n type: 'host',\n id: '${hostName}'\n});\n`
|
|
91
|
-
fs.writeFileSync(path.join(hostDir, 'rspack.config.mjs'), rspackContent);
|
|
92
|
-
|
|
93
|
-
console.log(`\nš Scaffolding Auth & Navigation...`);
|
|
94
|
-
fs.ensureDirSync(path.join(hostDir, 'providers'));
|
|
95
|
-
fs.ensureDirSync(path.join(hostDir, 'hooks'));
|
|
96
|
-
fs.ensureDirSync(path.join(hostDir, 'lib'));
|
|
97
|
-
fs.ensureDirSync(path.join(hostDir, 'app', '(protected)', 'module'));
|
|
98
|
-
|
|
99
|
-
fs.writeFileSync(path.join(hostDir, 'providers/auth.tsx'), templates.authProvider);
|
|
100
|
-
fs.writeFileSync(path.join(hostDir, 'app/_layout.tsx'), templates.rootLayout);
|
|
101
|
-
fs.writeFileSync(path.join(hostDir, 'app/login.tsx'), templates.loginPage);
|
|
102
|
-
fs.writeFileSync(path.join(hostDir, 'app/global.css'), templates.globalCss);
|
|
103
|
-
fs.writeFileSync(path.join(hostDir, 'lib/moduleLoader.ts'), templates.moduleLoader);
|
|
104
|
-
fs.writeFileSync(path.join(hostDir, 'index.js'), templates.indexJs);
|
|
105
|
-
fs.writeFileSync(path.join(hostDir, 'app/(protected)/index.tsx'), templates.dashboard);
|
|
106
|
-
fs.writeFileSync(path.join(hostDir, 'app/(protected)/module/[id].tsx'), templates.modulePage);
|
|
107
|
-
|
|
108
|
-
console.log(`\nš ESAD Workpace Initialized!`);
|
|
109
|
-
console.log(`-> cd ${projectName}\n-> esad dev (to start Host)`);
|
|
54
|
+
console.log(`\nš ESAD Workspace Initialized!`);
|
|
55
|
+
console.log(`-> cd ${projectName}/${hostName}\n-> esad host dev (to start Host)`);
|
|
110
56
|
} catch (err) {
|
|
111
57
|
console.error(`ā Failed to init Host:`, err.message);
|
|
112
58
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { runProcess } = require('./process');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Clones a template repository and cleans up the .git folder
|
|
7
|
+
*/
|
|
8
|
+
async function cloneTemplate(url, dest) {
|
|
9
|
+
console.log(`\nš„ Cloning template: ${url}...`);
|
|
10
|
+
await runProcess('git', ['clone', url, dest]);
|
|
11
|
+
|
|
12
|
+
const gitDir = path.join(dest, '.git');
|
|
13
|
+
if (fs.existsSync(gitDir)) {
|
|
14
|
+
await fs.remove(gitDir);
|
|
15
|
+
console.log(`ā
Detached from template repository.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Renames the project in package.json and app.json
|
|
21
|
+
*/
|
|
22
|
+
async function renameProject(targetDir, newName) {
|
|
23
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
24
|
+
const appJsonPath = path.join(targetDir, 'app.json');
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(pkgPath)) {
|
|
27
|
+
const pkg = await fs.readJson(pkgPath);
|
|
28
|
+
pkg.name = newName;
|
|
29
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
30
|
+
console.log(`ā
Updated package.json name: ${newName}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(appJsonPath)) {
|
|
34
|
+
const appJson = await fs.readJson(appJsonPath);
|
|
35
|
+
if (appJson.expo) {
|
|
36
|
+
appJson.expo.name = newName;
|
|
37
|
+
appJson.expo.slug = newName;
|
|
38
|
+
if (appJson.expo.android) {
|
|
39
|
+
appJson.expo.android.package = `com.anonymous.${newName.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
appJson.name = newName;
|
|
43
|
+
appJson.slug = newName;
|
|
44
|
+
}
|
|
45
|
+
await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
46
|
+
console.log(`ā
Updated app.json name/slug/package.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prepares the native folders and applies Re.Pack patches
|
|
52
|
+
*/
|
|
53
|
+
async function prepareNative(cwd, platform = 'android') {
|
|
54
|
+
if (!fs.existsSync(path.join(cwd, 'android')) && (platform === 'android' || platform === 'all')) {
|
|
55
|
+
console.log(`š¦ Native folder not found. Running expo prebuild...`);
|
|
56
|
+
await runProcess('npx', ['expo', 'prebuild', '--platform', 'android'], cwd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Apply Gradle Patch (Android)
|
|
60
|
+
const buildGradlePath = path.join(cwd, 'android/app/build.gradle');
|
|
61
|
+
if (fs.existsSync(buildGradlePath)) {
|
|
62
|
+
let content = await fs.readFile(buildGradlePath, 'utf8');
|
|
63
|
+
if (!content.includes('project.ext.react')) {
|
|
64
|
+
const patch = `\nproject.ext.react = [\n bundleCommand: "repack-bundle",\n bundleConfig: "rspack.config.mjs"\n]\n\n`;
|
|
65
|
+
content = content.replace(/react \{/, `${patch}react {`);
|
|
66
|
+
await fs.writeFile(buildGradlePath, content);
|
|
67
|
+
console.log(`ā
Patched android/app/build.gradle for Re.Pack.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create react-native.config.js if missing
|
|
72
|
+
const rnConfigPath = path.join(cwd, 'react-native.config.js');
|
|
73
|
+
if (!fs.existsSync(rnConfigPath)) {
|
|
74
|
+
const content = `module.exports = {\n commands: require('@callstack/repack/commands/rspack'),\n};\n`;
|
|
75
|
+
await fs.writeFile(rnConfigPath, content);
|
|
76
|
+
console.log(`ā
Generated react-native.config.js.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
cloneTemplate,
|
|
82
|
+
renameProject,
|
|
83
|
+
prepareNative
|
|
84
|
+
};
|
package/src/plugin/index.js
CHANGED
|
@@ -1,129 +1,128 @@
|
|
|
1
|
-
const path = require('node:path');
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const Repack = require('@callstack/repack');
|
|
4
|
-
const { ExpoModulesPlugin } = require('@callstack/repack-plugin-expo-modules');
|
|
5
|
-
const { ProvidePlugin, DefinePlugin } = require('@rspack/core');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* ESAD Re.Pack Plugin Wrapper
|
|
9
|
-
* Abstracts away the boilerplate of Module Federation and SDK integration for SuperApps.
|
|
10
|
-
*
|
|
11
|
-
* @param {Object} env Rspack environment
|
|
12
|
-
* @param {Object} options
|
|
13
|
-
* @param {string} options.type 'host' | 'module'
|
|
14
|
-
* @param {string} options.id Unique module or host ID
|
|
15
|
-
* @param {string} options.dirname Base directory (__dirname)
|
|
16
|
-
* @param {Object} [options.shared] Additional shared dependencies
|
|
17
|
-
* @param {Object} [options.exposes] Modules to expose (for modules)
|
|
18
|
-
* @param {Object} [options.remotes] Remote modules (for host)
|
|
19
|
-
*/
|
|
20
|
-
function withESAD(env, options) {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'@module-federation/
|
|
41
|
-
'@module-federation/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
...
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
/node_modules[\\/]react-native/,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
new
|
|
85
|
-
new Repack.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
'react': { singleton: true, eager: true, requiredVersion: pkg.dependencies.react },
|
|
95
|
-
'react
|
|
96
|
-
'react-native': { singleton: true, eager: true, requiredVersion: pkg.dependencies['react-native'] },
|
|
97
|
-
'
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
res.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
module.exports = { withESAD };
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const Repack = require('@callstack/repack');
|
|
4
|
+
const { ExpoModulesPlugin } = require('@callstack/repack-plugin-expo-modules');
|
|
5
|
+
const { ProvidePlugin, DefinePlugin } = require('@rspack/core');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ESAD Re.Pack Plugin Wrapper
|
|
9
|
+
* Abstracts away the boilerplate of Module Federation and SDK integration for SuperApps.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} env Rspack environment
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} options.type 'host' | 'module'
|
|
14
|
+
* @param {string} options.id Unique module or host ID
|
|
15
|
+
* @param {string} options.dirname Base directory (__dirname)
|
|
16
|
+
* @param {Object} [options.shared] Additional shared dependencies
|
|
17
|
+
* @param {Object} [options.exposes] Modules to expose (for modules)
|
|
18
|
+
* @param {Object} [options.remotes] Remote modules (for host)
|
|
19
|
+
*/
|
|
20
|
+
function withESAD(env, options) {
|
|
21
|
+
const { platform, dev } = env;
|
|
22
|
+
const isDev = dev !== false;
|
|
23
|
+
const dirname = options.dirname;
|
|
24
|
+
const pkgPath = path.resolve(dirname, 'package.json');
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
26
|
+
const id = options.id.replace(/-/g, '_');
|
|
27
|
+
|
|
28
|
+
console.log(`[ESAD] Applying Mega-Zero-Config profile for ${options.type.toUpperCase()} (${platform}): ${id}`);
|
|
29
|
+
|
|
30
|
+
const config = {
|
|
31
|
+
mode: isDev ? 'development' : 'production',
|
|
32
|
+
context: dirname,
|
|
33
|
+
entry: options.entry || './index.js',
|
|
34
|
+
resolve: {
|
|
35
|
+
...Repack.getResolveOptions(),
|
|
36
|
+
alias: {
|
|
37
|
+
'@': path.resolve(dirname, '.'),
|
|
38
|
+
// Internal MFv2 & Re.Pack Aliases (Magic)
|
|
39
|
+
'@module-federation/runtime/helpers': path.resolve(dirname, 'node_modules/@module-federation/runtime/dist/helpers.js'),
|
|
40
|
+
'@module-federation/error-codes/browser': path.resolve(dirname, 'node_modules/@module-federation/error-codes/dist/browser.cjs'),
|
|
41
|
+
'@module-federation/sdk': path.resolve(dirname, 'node_modules/@module-federation/sdk'),
|
|
42
|
+
|
|
43
|
+
// ESAD SDK Aliases (Zero-Config)
|
|
44
|
+
'@codemoreira/esad/client': path.resolve(dirname, 'node_modules/@codemoreira/esad/src/client/index.js'),
|
|
45
|
+
|
|
46
|
+
...Repack.getResolveOptions().alias,
|
|
47
|
+
...(options.alias || {}),
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
module: {
|
|
51
|
+
rules: [
|
|
52
|
+
{
|
|
53
|
+
oneOf: [
|
|
54
|
+
{
|
|
55
|
+
test: /\.[cm]?[jt]sx?$/,
|
|
56
|
+
include: [
|
|
57
|
+
/node_modules[\\/]react-native/,
|
|
58
|
+
/node_modules[\\/]@react-native/,
|
|
59
|
+
],
|
|
60
|
+
type: 'javascript/auto',
|
|
61
|
+
use: {
|
|
62
|
+
loader: '@callstack/repack/babel-swc-loader',
|
|
63
|
+
options: {
|
|
64
|
+
sourceMaps: true,
|
|
65
|
+
parallel: true,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
...Repack.getJsTransformRules(),
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
...Repack.getAssetTransformRules(),
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
plugins: [
|
|
76
|
+
new ProvidePlugin({
|
|
77
|
+
process: 'process/browser',
|
|
78
|
+
}),
|
|
79
|
+
new DefinePlugin({
|
|
80
|
+
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
|
81
|
+
'__DEV__': JSON.stringify(isDev),
|
|
82
|
+
}),
|
|
83
|
+
new ExpoModulesPlugin(),
|
|
84
|
+
new Repack.RepackPlugin(),
|
|
85
|
+
new Repack.plugins.ModuleFederationPluginV2({
|
|
86
|
+
name: id,
|
|
87
|
+
filename: `${id}.container.js.bundle`,
|
|
88
|
+
remotes: options.remotes || {},
|
|
89
|
+
...(options.type === 'module' ? { exposes: options.exposes || {} } : {}),
|
|
90
|
+
dts: false,
|
|
91
|
+
dev: isDev,
|
|
92
|
+
shared: {
|
|
93
|
+
'react': { singleton: true, eager: true, requiredVersion: pkg.dependencies.react },
|
|
94
|
+
'react/jsx-runtime': { singleton: true, eager: true, requiredVersion: pkg.dependencies.react },
|
|
95
|
+
'react-native': { singleton: true, eager: true, requiredVersion: pkg.dependencies['react-native'] },
|
|
96
|
+
'react-native-safe-area-context': { singleton: true, eager: true, requiredVersion: pkg.dependencies['react-native-safe-area-context'] },
|
|
97
|
+
'@codemoreira/esad': { singleton: true, eager: true },
|
|
98
|
+
...(options.shared || {})
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Add Host-specific DevServer magic for Expo
|
|
105
|
+
if (options.type === 'host') {
|
|
106
|
+
config.devServer = {
|
|
107
|
+
setupMiddlewares: (middlewares) => {
|
|
108
|
+
middlewares.unshift((req, res, next) => {
|
|
109
|
+
if (req.url.startsWith('/.expo/.virtual-metro-entry.bundle')) {
|
|
110
|
+
const query = req.url.split('?')[1];
|
|
111
|
+
const isMap = req.url.includes('.map');
|
|
112
|
+
const target = isMap ? '/index.bundle.map' : '/index.bundle';
|
|
113
|
+
const location = query ? `${target}?${query}` : target;
|
|
114
|
+
res.writeHead(302, { Location: location });
|
|
115
|
+
res.end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
next();
|
|
119
|
+
});
|
|
120
|
+
return middlewares;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { withESAD };
|
|
@@ -1,201 +0,0 @@
|
|
|
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
|
-
|
|
200
|
-
indexJs: `import "expo-router/entry";`
|
|
201
|
-
};
|