@codemoreira/esad 1.2.3 ā 1.2.5
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 +19 -5
- package/src/cli/commands/createModule.js +47 -47
- package/src/cli/commands/deploy.js +34 -13
- package/src/cli/commands/dev.js +29 -8
- package/src/cli/commands/host.js +26 -6
- package/src/cli/commands/init.js +113 -113
- package/src/cli/templates/host.js +201 -201
- package/src/client/index.d.ts +11 -0
- package/src/plugin/index.d.ts +9 -0
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemoreira/esad",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
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
|
+
"types": "./src/plugin/index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
|
-
".":
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
}
|
|
10
20
|
},
|
|
11
21
|
"bin": {
|
|
12
22
|
"esad": "./bin/esad.js"
|
|
@@ -26,5 +36,9 @@
|
|
|
26
36
|
"cross-spawn": "^7.0.3",
|
|
27
37
|
"fs-extra": "^11.2.0"
|
|
28
38
|
},
|
|
29
|
-
"devDependencies": {
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@rspack/core": "^1.7.9",
|
|
41
|
+
"@types/react": "^19.2.14",
|
|
42
|
+
"@types/react-native": "^0.72.8"
|
|
43
|
+
}
|
|
30
44
|
}
|
|
@@ -1,47 +1,47 @@
|
|
|
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
|
-
// 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.`);
|
|
42
|
-
|
|
43
|
-
console.log(`\nš Module ${finalModuleName} is ready!`);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
console.error(`ā Failed to scaffold module`, err.message);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
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
|
+
// 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.`);
|
|
42
|
+
|
|
43
|
+
console.log(`\nš Module ${finalModuleName} is ready!`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`ā Failed to scaffold module`, err.message);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -4,9 +4,36 @@ const AdmZip = require('adm-zip');
|
|
|
4
4
|
const { getWorkspaceConfig } = require('../utils/config');
|
|
5
5
|
|
|
6
6
|
module.exports = async (options) => {
|
|
7
|
-
|
|
7
|
+
let cwd = process.cwd();
|
|
8
|
+
let pkgPath = path.join(cwd, 'package.json');
|
|
9
|
+
|
|
10
|
+
// Try to find workspace config for root-level execution
|
|
11
|
+
const configObj = getWorkspaceConfig();
|
|
12
|
+
if (configObj) {
|
|
13
|
+
const workspaceRoot = path.dirname(configObj.path);
|
|
14
|
+
const { projectName } = configObj.data;
|
|
15
|
+
|
|
16
|
+
// If ID is provided, try to find that module/host folder
|
|
17
|
+
if (options.id) {
|
|
18
|
+
const targetDir = path.join(workspaceRoot, options.id);
|
|
19
|
+
if (fs.existsSync(targetDir)) {
|
|
20
|
+
cwd = targetDir;
|
|
21
|
+
pkgPath = path.join(cwd, 'package.json');
|
|
22
|
+
console.log(`š Auto-detected Project folder: ${path.relative(process.cwd(), targetDir)}`);
|
|
23
|
+
}
|
|
24
|
+
} else if (!fs.existsSync(pkgPath)) {
|
|
25
|
+
// If no ID and no package.json in current dir, assume we want to deploy the host from root
|
|
26
|
+
const hostDir = path.join(workspaceRoot, `${projectName}-host`);
|
|
27
|
+
if (fs.existsSync(hostDir)) {
|
|
28
|
+
cwd = hostDir;
|
|
29
|
+
pkgPath = path.join(cwd, 'package.json');
|
|
30
|
+
console.log(`š Auto-detected Host App folder: ${path.relative(process.cwd(), hostDir)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
if (!fs.existsSync(pkgPath)) {
|
|
9
|
-
console.error(`ā Error:
|
|
36
|
+
console.error(`ā Error: Call this command from inside a Project directory or the Workspace Root.`);
|
|
10
37
|
process.exit(1);
|
|
11
38
|
}
|
|
12
39
|
|
|
@@ -17,14 +44,8 @@ module.exports = async (options) => {
|
|
|
17
44
|
|
|
18
45
|
console.log(`\nāļø Starting ESAD Deploy for ${moduleId} (v${version})\n`);
|
|
19
46
|
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
22
|
-
console.error(`ā Error: esad.config.json not found in current directory or parent.`);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const config = configObj.data;
|
|
27
|
-
if (!config.deployEndpoint) {
|
|
47
|
+
const config = configObj ? configObj.data : null;
|
|
48
|
+
if (!config?.deployEndpoint) {
|
|
28
49
|
console.error(`ā Error: 'deployEndpoint' not configured in esad.config.json.`);
|
|
29
50
|
process.exit(1);
|
|
30
51
|
}
|
|
@@ -32,16 +53,16 @@ module.exports = async (options) => {
|
|
|
32
53
|
const deployUrl = config.deployEndpoint.replace('{{moduleId}}', moduleId);
|
|
33
54
|
console.log(`š” Deployment Endpoint Resolved: ${deployUrl}`);
|
|
34
55
|
|
|
35
|
-
const distPath = path.join(
|
|
56
|
+
const distPath = path.join(cwd, 'dist');
|
|
36
57
|
if (!fs.existsSync(distPath)) {
|
|
37
|
-
console.error(`ā Error: dist/ directory not found. Did you run the build command?`);
|
|
58
|
+
console.error(`ā Error: dist/ directory not found in ${cwd}. Did you run the build command?`);
|
|
38
59
|
process.exit(1);
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
const zip = new AdmZip();
|
|
42
63
|
zip.addLocalFolder(distPath);
|
|
43
64
|
|
|
44
|
-
const zipPath = path.join(
|
|
65
|
+
const zipPath = path.join(cwd, `bundle-${moduleId}-${version}.zip`);
|
|
45
66
|
zip.writeZip(zipPath);
|
|
46
67
|
console.log(`šļø Zipped output to ${zipPath}`);
|
|
47
68
|
|
package/src/cli/commands/dev.js
CHANGED
|
@@ -4,12 +4,34 @@ const fs = require('fs-extra');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
module.exports = async (options) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
console.error(`ā Error: Call this command from inside a Host or Module directory.`);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
7
|
+
let cwd = process.cwd();
|
|
8
|
+
let pkgPath = path.join(cwd, 'package.json');
|
|
12
9
|
|
|
10
|
+
// Try to find workspace config for root-level execution
|
|
11
|
+
const configObj = getWorkspaceConfig();
|
|
12
|
+
if (configObj) {
|
|
13
|
+
const workspaceRoot = path.dirname(configObj.path);
|
|
14
|
+
const { projectName } = configObj.data;
|
|
15
|
+
|
|
16
|
+
if (options.id) {
|
|
17
|
+
// Target a specific module
|
|
18
|
+
const moduleDir = path.join(workspaceRoot, options.id);
|
|
19
|
+
if (fs.existsSync(moduleDir)) {
|
|
20
|
+
cwd = moduleDir;
|
|
21
|
+
pkgPath = path.join(cwd, 'package.json');
|
|
22
|
+
console.log(`š Auto-detected Module folder: ${path.relative(process.cwd(), moduleDir)}`);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
// Target host by default if in root
|
|
26
|
+
const hostDir = path.join(workspaceRoot, `${projectName}-host`);
|
|
27
|
+
if (fs.existsSync(hostDir)) {
|
|
28
|
+
cwd = hostDir;
|
|
29
|
+
pkgPath = path.join(cwd, 'package.json');
|
|
30
|
+
console.log(`š Auto-detected Host App folder: ${path.relative(process.cwd(), hostDir)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
const pkg = fs.readJsonSync(pkgPath);
|
|
14
36
|
const moduleId = options.id || pkg.name;
|
|
15
37
|
const port = options.port || '8081';
|
|
@@ -19,13 +41,12 @@ module.exports = async (options) => {
|
|
|
19
41
|
|
|
20
42
|
if (isHost && !options.id) {
|
|
21
43
|
console.log(`\nš Starting Host App Dev Server (Re.Pack/Rspack)...\n`);
|
|
22
|
-
await spawn('npx', ['react-native', 'webpack-start'], { stdio: 'inherit', shell: true });
|
|
44
|
+
await spawn('npx', ['react-native', 'webpack-start'], { cwd, stdio: 'inherit', shell: true });
|
|
23
45
|
return;
|
|
24
46
|
}
|
|
25
47
|
|
|
26
48
|
console.log(`\nā” Starting ESAD Dev Server for ${moduleId} on port ${port}...\n`);
|
|
27
49
|
|
|
28
|
-
const configObj = getWorkspaceConfig();
|
|
29
50
|
const config = configObj ? configObj.data : null;
|
|
30
51
|
let devApiUrl = config?.devModeEndpoint ? config.devModeEndpoint.replace('{{moduleId}}', moduleId) : null;
|
|
31
52
|
|
|
@@ -43,7 +64,7 @@ module.exports = async (options) => {
|
|
|
43
64
|
|
|
44
65
|
await setDevMode(true);
|
|
45
66
|
|
|
46
|
-
const proc = spawn('npx', ['react-native', 'webpack-start', '--port', port], { stdio: 'inherit', shell: true });
|
|
67
|
+
const proc = spawn('npx', ['react-native', 'webpack-start', '--port', port], { cwd, stdio: 'inherit', shell: true });
|
|
47
68
|
|
|
48
69
|
const shutdown = async () => {
|
|
49
70
|
console.log(`\nš Parando ESAD Dev Server e revertendo o registro na CDN...`);
|
package/src/cli/commands/host.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs-extra');
|
|
|
4
4
|
const { spawn } = require('cross-spawn');
|
|
5
5
|
const http = require('http');
|
|
6
6
|
const readline = require('readline');
|
|
7
|
+
const { getWorkspaceConfig } = require('../utils/config');
|
|
7
8
|
|
|
8
9
|
const rl = readline.createInterface({
|
|
9
10
|
input: process.stdin,
|
|
@@ -13,11 +14,25 @@ const rl = readline.createInterface({
|
|
|
13
14
|
const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
14
15
|
|
|
15
16
|
module.exports = async (subcommand) => {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
let cwd = process.cwd();
|
|
18
|
+
let pkgPath = path.join(cwd, 'package.json');
|
|
18
19
|
|
|
20
|
+
// Try to find workspace config to resolve host path from root
|
|
21
|
+
const configObj = getWorkspaceConfig();
|
|
22
|
+
if (configObj) {
|
|
23
|
+
const workspaceRoot = path.dirname(configObj.path);
|
|
24
|
+
const { projectName } = configObj.data;
|
|
25
|
+
const hostDir = path.join(workspaceRoot, `${projectName}-host`);
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync(hostDir)) {
|
|
28
|
+
cwd = hostDir;
|
|
29
|
+
pkgPath = path.join(cwd, 'package.json');
|
|
30
|
+
console.log(`š Auto-detected Host App folder: ${path.relative(process.cwd(), hostDir)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
if (!fs.existsSync(pkgPath)) {
|
|
20
|
-
console.error(`ā Error: Call this command from inside the Host App
|
|
35
|
+
console.error(`ā Error: Call this command from inside the Host App or the Workspace Root.`);
|
|
21
36
|
return;
|
|
22
37
|
}
|
|
23
38
|
|
|
@@ -85,15 +100,20 @@ module.exports = async (subcommand) => {
|
|
|
85
100
|
// 4. Start Bundler in a New Window
|
|
86
101
|
console.log(`\nš ļø Starting Rspack Bundler in a new window...`);
|
|
87
102
|
if (process.platform === 'win32') {
|
|
88
|
-
// Use CMD /C START to open a new window
|
|
89
|
-
spawn('cmd', ['/c', 'start', 'npx', 'react-native', 'webpack-start'], {
|
|
103
|
+
// Use CMD /C START /D <dir> to open a new window in the correct folder
|
|
104
|
+
spawn('cmd', ['/c', 'start', '/D', cwd, 'npx', 'react-native', 'webpack-start'], {
|
|
90
105
|
detached: true,
|
|
91
106
|
stdio: 'ignore',
|
|
92
107
|
shell: true
|
|
93
108
|
}).unref();
|
|
94
109
|
} else {
|
|
95
110
|
// For MacOS or Linux
|
|
96
|
-
spawn('npx', ['react-native', 'webpack-start'], {
|
|
111
|
+
spawn('npx', ['react-native', 'webpack-start'], {
|
|
112
|
+
cwd,
|
|
113
|
+
detached: true,
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
shell: true
|
|
116
|
+
}).unref();
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
// 5. Wait for Bundler (Port 8081)
|
package/src/cli/commands/init.js
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
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, Re.Pack and UI dependencies into host...`);
|
|
48
|
-
const hostPkgPath = path.join(hostDir, 'package.json');
|
|
49
|
-
const hostPkg = fs.readJsonSync(hostPkgPath);
|
|
50
|
-
const reactVersion = hostPkg.dependencies.react;
|
|
51
|
-
|
|
52
|
-
const deps = [
|
|
53
|
-
'@codemoreira/esad',
|
|
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);
|
|
71
|
-
|
|
72
|
-
// Re-read package.json to get the version after npm install updated it
|
|
73
|
-
const updatedPkg = fs.readJsonSync(hostPkgPath);
|
|
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)`);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
console.error(`ā Failed to init Host:`, err.message);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
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, Re.Pack and UI dependencies into host...`);
|
|
48
|
+
const hostPkgPath = path.join(hostDir, 'package.json');
|
|
49
|
+
const hostPkg = fs.readJsonSync(hostPkgPath);
|
|
50
|
+
const reactVersion = hostPkg.dependencies.react;
|
|
51
|
+
|
|
52
|
+
const deps = [
|
|
53
|
+
'@codemoreira/esad',
|
|
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);
|
|
71
|
+
|
|
72
|
+
// Re-read package.json to get the version after npm install updated it
|
|
73
|
+
const updatedPkg = fs.readJsonSync(hostPkgPath);
|
|
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)`);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`ā Failed to init Host:`, err.message);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -1,201 +1,201 @@
|
|
|
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
|
-
};
|
|
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
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ESADEventEmitter {
|
|
4
|
+
set(key: string, value: any): void;
|
|
5
|
+
get(key: string): any;
|
|
6
|
+
subscribe(key: string, callback: (value: any) => void): () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ESADState: ESADEventEmitter;
|
|
10
|
+
|
|
11
|
+
export function useESADState<T>(key: string, initialValue?: T): [T, Dispatch<SetStateAction<T>>];
|