@codemoreira/esad 1.4.6-1 → 1.4.6-11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/esad.js CHANGED
@@ -16,7 +16,7 @@ program
16
16
  .version(pkg.version)
17
17
  .description('esad - Easy Super App Development Toolkit');
18
18
 
19
- // --- COMMMAND: esad init ---
19
+ // --- COMMAND: esad init ---
20
20
  program
21
21
  .command('init <project-name>')
22
22
  .description('Scaffold a new ESAD workspace containing the Host App')
@@ -34,15 +34,52 @@ program
34
34
  process.exit(0);
35
35
  });
36
36
 
37
- // --- COMMAND: esad host ---
37
+ // --- COMMAND: esad dev ---
38
+ program
39
+ .command('dev [platform]')
40
+ .description('Start the development environment for the Host App (platform: android, ios)')
41
+ .action(async (platform) => {
42
+ await hostCommand(platform);
43
+ process.exit(0);
44
+ });
45
+
46
+ // --- COMMAND: esad android ---
38
47
  program
39
- .command('host <subcommand>')
40
- .description('Manage the Host App (Auto-Syncs config, runs android/ios)')
41
- .action(async (sub) => {
42
- await hostCommand(sub);
48
+ .command('android')
49
+ .description('Run Host on Android (alias for esad dev android)')
50
+ .action(async () => {
51
+ await hostCommand('android');
43
52
  process.exit(0);
44
53
  });
45
54
 
55
+ // --- COMMAND: esad ios ---
56
+ program
57
+ .command('ios')
58
+ .description('Run Host on iOS (alias for esad dev ios)')
59
+ .action(async () => {
60
+ await hostCommand('ios');
61
+ process.exit(0);
62
+ });
63
+
64
+ // --- COMMAND: esad start ---
65
+ program
66
+ .command('start')
67
+ .description('Alias for esad dev')
68
+ .action(async () => {
69
+ await hostCommand('dev');
70
+ process.exit(0);
71
+ });
72
+
73
+ // --- COMMAND: esad push ---
74
+ program
75
+ .command('push')
76
+ .option('-i, --id <moduleId>', 'The Module ID to sync to Dev-Cloud')
77
+ .option('-p, --platform <platform>', 'Platform (android, ios)', 'android')
78
+ .description('Build and Push the module bundle to the Dev-Cloud for global previewing')
79
+ .action(async (options) => {
80
+ await devCommand(options);
81
+ });
82
+
46
83
  // --- COMMAND: esad create-module ---
47
84
  program
48
85
  .command('create-module <module-name>')
@@ -75,15 +112,4 @@ program
75
112
  process.exit(0);
76
113
  });
77
114
 
78
- // --- COMMAND: esad dev ---
79
- program
80
- .command('dev')
81
- .option('-i, --id <moduleId>', 'The Module ID to sync to Dev-Cloud')
82
- .option('-p, --platform <platform>', 'Platform (android, ios)', 'android')
83
- .description('Build and Push the module bundle to the Dev-Cloud for global previewing')
84
- .action(async (options) => {
85
- await devCommand(options);
86
- // Note: dev command has its own shutdown logic with SIGINT/SIGTERM
87
- });
88
-
89
115
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemoreira/esad",
3
- "version": "1.4.6-1",
3
+ "version": "1.4.6-11",
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
  "types": "./src/plugin/index.d.ts",
@@ -16,7 +16,8 @@
16
16
  "./client": {
17
17
  "types": "./src/client/index.d.ts",
18
18
  "default": "./src/client/index.js"
19
- }
19
+ },
20
+ "./config-plugin": "./src/plugin/config-plugin.js"
20
21
  },
21
22
  "bin": {
22
23
  "esad": "./bin/esad.js"
@@ -46,7 +47,11 @@
46
47
  "react-native-reanimated": "~3.16.1",
47
48
  "form-data": "^4.0.0",
48
49
  "fs-extra": "^11.2.0",
49
- "node-fetch": "^2.7.0"
50
+ "node-fetch": "^2.7.0",
51
+ "@babel/plugin-transform-flow-strip-types": "^7.20.0",
52
+ "@babel/plugin-transform-private-methods": "^7.20.0",
53
+ "@babel/plugin-transform-private-property-in-object": "^7.20.0",
54
+ "@babel/plugin-transform-class-properties": "^7.20.0"
50
55
  },
51
56
  "devDependencies": {
52
57
  "@rspack/core": "^1.7.9",
@@ -52,7 +52,8 @@ module.exports = async (options) => {
52
52
  '--platform', platform,
53
53
  '--dev', 'false',
54
54
  '--bundle-output', bundleOutput,
55
- '--assets-dest', path.dirname(bundleOutput)
55
+ '--assets-dest', path.dirname(bundleOutput),
56
+ '--reset-cache'
56
57
  ], cwd);
57
58
 
58
59
  console.log(chalk.green(`\n✅ Build complete! Assets generated in build/ directory.`));
@@ -84,7 +84,8 @@ module.exports = async (options) => {
84
84
  '--platform', platform,
85
85
  '--dev', 'false',
86
86
  '--bundle-output', bundleOutput,
87
- '--assets-dest', path.dirname(bundleOutput)
87
+ '--assets-dest', path.dirname(bundleOutput),
88
+ '--reset-cache'
88
89
  ], cwd);
89
90
  } catch (err) {
90
91
  console.error(chalk.red(`❌ Build failed.`));
@@ -7,12 +7,7 @@ const readline = require('readline');
7
7
  const { getWorkspaceConfig } = require('../utils/config');
8
8
  const { prepareNative } = require('../utils/scaffold');
9
9
 
10
- const rl = readline.createInterface({
11
- input: process.stdin,
12
- output: process.stdout
13
- });
14
-
15
- const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
10
+ const askQuestion = (query, rl) => new Promise((resolve) => rl.question(query, resolve));
16
11
 
17
12
  /**
18
13
  * Check if a port is in use
@@ -28,6 +23,12 @@ const isPortInUse = (port) => new Promise((resolve) => {
28
23
  });
29
24
 
30
25
  module.exports = async (subcommand) => {
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout
29
+ });
30
+
31
+ if (!subcommand) subcommand = 'dev';
31
32
  let cwd = process.cwd();
32
33
  let pkgPath = path.join(cwd, 'package.json');
33
34
 
@@ -64,7 +65,7 @@ module.exports = async (subcommand) => {
64
65
  console.log(`[b] Bundler Only`);
65
66
  console.log(`[c] Cancel`);
66
67
 
67
- const choice = (await askQuestion(`\nSelect platform: `)).toLowerCase();
68
+ const choice = (await askQuestion(`\nSelect platform: `, rl)).toLowerCase();
68
69
 
69
70
  if (choice === 'c') {
70
71
  console.log(`\n❌ Cancelled.`);
@@ -86,13 +87,13 @@ module.exports = async (subcommand) => {
86
87
  if (shouldStartBundler && choice !== 'c') {
87
88
  console.log(`\n🛠️ Starting Rspack Bundler in a new window...`);
88
89
  if (process.platform === 'win32') {
89
- spawn('cmd', ['/c', 'start', '/D', cwd, 'npx', 'react-native', 'webpack-start'], {
90
+ spawn('cmd', ['/c', 'start', '/D', cwd, 'npx', 'react-native', 'start', '--reset-cache'], {
90
91
  detached: true,
91
92
  stdio: 'ignore',
92
93
  shell: true
93
94
  }).unref();
94
95
  } else {
95
- spawn('npx', ['react-native', 'webpack-start'], {
96
+ spawn('npx', ['react-native', 'start', '--reset-cache'], {
96
97
  cwd,
97
98
  detached: true,
98
99
  stdio: 'inherit',
@@ -139,13 +140,16 @@ module.exports = async (subcommand) => {
139
140
  // Other subcommands (android, ios directly)
140
141
  try {
141
142
  if (subcommand === 'android') {
143
+ console.log(`🤖 Compiling and launching on Android...`);
142
144
  await runProcess('npx', ['expo', 'run:android', '--no-bundler'], cwd);
143
145
  } else if (subcommand === 'ios') {
146
+ console.log(`🍎 Compiling and launching on iOS...`);
144
147
  await runProcess('npx', ['expo', 'run:ios', '--no-bundler'], cwd);
145
148
  }
146
149
  } catch (err) {
147
150
  console.error(`❌ Error running host command: ${err.message}`);
151
+ } finally {
152
+ rl.close();
148
153
  }
149
- rl.close();
150
154
  }
151
155
  };
@@ -8,8 +8,9 @@ const { spawn } = require('cross-spawn');
8
8
  * @returns {Promise<void>}
9
9
  */
10
10
  const runProcess = (cmd, args, cwd = process.cwd()) => {
11
+ const executable = (process.platform === 'win32' && cmd === 'npx') ? 'npx.cmd' : cmd;
11
12
  return new Promise((resolve, reject) => {
12
- const child = spawn(cmd, args, { stdio: 'inherit', cwd, shell: true });
13
+ const child = spawn(executable, args, { stdio: 'inherit', cwd, shell: true });
13
14
  child.on('close', code => {
14
15
  if (code !== 0) reject(new Error(`Command ${cmd} ${args.join(' ')} failed`));
15
16
  else resolve();
@@ -35,15 +35,21 @@ async function renameProject(targetDir, newName) {
35
35
  if (appJson.expo) {
36
36
  appJson.expo.name = newName;
37
37
  appJson.expo.slug = newName;
38
+ appJson.expo.scheme = newName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
39
+
38
40
  if (appJson.expo.android) {
39
41
  appJson.expo.android.package = `com.anonymous.${newName.replace(/[^a-zA-Z0-9]/g, '')}`;
40
42
  }
43
+
44
+ if (appJson.expo.ios) {
45
+ appJson.expo.ios.bundleIdentifier = `com.anonymous.${newName.replace(/[^a-zA-Z0-9]/g, '')}`;
46
+ }
41
47
  } else {
42
48
  appJson.name = newName;
43
49
  appJson.slug = newName;
44
50
  }
45
51
  await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
46
- console.log(`✅ Updated app.json name/slug/package.`);
52
+ console.log(`✅ Updated app.json name/slug/package/scheme.`);
47
53
  }
48
54
 
49
55
  // 3. Update Rspack Config if exists
@@ -60,27 +66,37 @@ async function renameProject(targetDir, newName) {
60
66
  }
61
67
 
62
68
  /**
63
- * Prepares the native folders and applies Re.Pack patches
69
+ * Prepares the native folders and ensures the ESAD Config Plugin is registered
64
70
  */
65
71
  async function prepareNative(cwd, platform = 'android') {
72
+ const appJsonPath = path.join(cwd, 'app.json');
73
+
74
+ // 1. Ensure Config Plugin is in app.json
75
+ if (fs.existsSync(appJsonPath)) {
76
+ const appJson = await fs.readJson(appJsonPath);
77
+ if (appJson.expo) {
78
+ const plugins = appJson.expo.plugins || [];
79
+ const pluginName = '@codemoreira/esad/config-plugin';
80
+
81
+ if (!plugins.includes(pluginName) && !plugins.some(p => Array.isArray(p) && p[0] === pluginName)) {
82
+ appJson.expo.plugins = [...plugins, pluginName];
83
+ await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
84
+ console.log(`✅ Added ESAD Config Plugin to app.json.`);
85
+ }
86
+ }
87
+ }
88
+
89
+ // 2. Run Prebuild (this will trigger the plugin)
66
90
  if (!fs.existsSync(path.join(cwd, 'android')) && (platform === 'android' || platform === 'all')) {
67
91
  console.log(`📦 Native folder not found. Running expo prebuild...`);
68
92
  await runProcess('npx', ['expo', 'prebuild', '--platform', 'android'], cwd);
93
+ } else if (platform === 'all' || platform === 'ios' || platform === 'android') {
94
+ // If folder exists, we still might want to run prebuild to sync changes if forced,
95
+ // but ESAD's dev command usually assumes it's managed.
96
+ // For now, let's just ensure react-native.config.js exists.
69
97
  }
70
98
 
71
- // Apply Gradle Patch (Android)
72
- const buildGradlePath = path.join(cwd, 'android/app/build.gradle');
73
- if (fs.existsSync(buildGradlePath)) {
74
- let content = await fs.readFile(buildGradlePath, 'utf8');
75
- if (!content.includes('project.ext.react')) {
76
- const patch = `\nproject.ext.react = [\n bundleCommand: "repack-bundle",\n bundleConfig: "rspack.config.mjs"\n]\n\n`;
77
- content = content.replace(/react \{/, `${patch}react {`);
78
- await fs.writeFile(buildGradlePath, content);
79
- console.log(`✅ Patched android/app/build.gradle for Re.Pack.`);
80
- }
81
- }
82
-
83
- // Create react-native.config.js if missing
99
+ // 3. Create react-native.config.js if missing
84
100
  const rnConfigPath = path.join(cwd, 'react-native.config.js');
85
101
  if (!fs.existsSync(rnConfigPath)) {
86
102
  const content = `module.exports = {\n commands: require('@callstack/repack/commands/rspack'),\n};\n`;
@@ -0,0 +1,45 @@
1
+ const { withAppBuildGradle, withXcodeProject } = require('expo/config-plugins');
2
+
3
+ /**
4
+ * ESAD Re.Pack Config Plugin
5
+ *
6
+ * Automates the native patching required for Re.Pack/Rspack to work with Expo.
7
+ * Replaces manual patching in ESAD CLI.
8
+ */
9
+ module.exports = (config) => {
10
+ // --- iOS Patching ---
11
+ config = withXcodeProject(config, (configuration) => {
12
+ const xcodeProject = configuration.modResults;
13
+ const bundleReactNativeCodeAndImagesBuildPhase = xcodeProject.buildPhaseObject(
14
+ 'PBXShellScriptBuildPhase',
15
+ 'Bundle React Native code and images'
16
+ );
17
+
18
+ if (bundleReactNativeCodeAndImagesBuildPhase) {
19
+ const script = JSON.parse(bundleReactNativeCodeAndImagesBuildPhase.shellScript);
20
+ const patched = script
21
+ .replace(
22
+ /if \[\[ -z "\$CLI_PATH" \]\]; then[\s\S]*?fi\n?/g,
23
+ `export CLI_PATH="$("$NODE_BINARY" --print "require('path').dirname(require.resolve('@react-native-community/cli/package.json')) + '/build/bin.js'")"`
24
+ )
25
+ .replace(/if \[\[ -z "\$BUNDLE_COMMAND" \]\]; then[\s\S]*?fi\n?/g, '');
26
+
27
+ bundleReactNativeCodeAndImagesBuildPhase.shellScript = JSON.stringify(patched);
28
+ }
29
+ return configuration;
30
+ });
31
+
32
+ // --- Android Patching ---
33
+ config = withAppBuildGradle(config, (configuration) => {
34
+ const buildGradle = configuration.modResults.contents;
35
+ // Ensure Re.Pack bundle command is used
36
+ const patched = buildGradle
37
+ .replace(/cliFile.*/, '')
38
+ .replace(/bundleCommand.*/, 'bundleCommand = "bundle"');
39
+
40
+ configuration.modResults.contents = patched;
41
+ return configuration;
42
+ });
43
+
44
+ return config;
45
+ };
@@ -3,6 +3,7 @@ const fs = require('node:fs');
3
3
  const Repack = require('@callstack/repack');
4
4
  const { ExpoModulesPlugin } = require('@callstack/repack-plugin-expo-modules');
5
5
  const { ProvidePlugin, DefinePlugin } = require('@rspack/core');
6
+ const { ReanimatedPlugin } = require('@callstack/repack-plugin-reanimated');
6
7
 
7
8
  /**
8
9
  * ESAD Re.Pack Plugin Wrapper
@@ -38,41 +39,30 @@ function withESAD(env, options) {
38
39
  clean: true,
39
40
  },
40
41
  resolve: {
41
- ...Repack.getResolveOptions(),
42
- extensions: [
43
- '.expo.ts', '.expo.tsx', '.expo.js', '.expo.jsx',
44
- '.native.ts', '.native.tsx', '.native.js', '.native.jsx',
45
- ...Repack.getResolveOptions().extensions,
46
- ],
42
+ ...Repack.getResolveOptions(platform),
47
43
  alias: {
44
+ '~': dirname,
48
45
  '@': dirname,
49
- 'expo-router': path.resolve(dirname, 'node_modules/expo-router'),
50
- 'react-native': path.resolve(dirname, 'node_modules/react-native'),
51
- ...Repack.getResolveOptions().alias,
52
- }
46
+ '@module-federation/runtime/helpers': path.resolve(dirname, 'node_modules/@module-federation/runtime/dist/helpers.cjs'),
47
+ '@module-federation/error-codes/browser': path.resolve(dirname, 'node_modules/@module-federation/error-codes/dist/browser.cjs'),
48
+ ...Repack.getResolveOptions(platform).alias,
49
+ },
50
+ conditionNames: ['react-native', 'require', 'import', 'default'],
53
51
  },
54
52
  module: {
55
53
  rules: [
56
54
  {
57
- oneOf: [
58
- {
59
- test: /\.[jt]sx?$/,
60
- include: [
61
- path.resolve(dirname, 'app'),
62
- path.resolve(dirname, 'index.js'),
63
- /[\\/]node_modules[\\/](expo-router|react-native|@react-native|expo-modules-core)[\\/]/
64
- ],
65
- use: {
66
- loader: 'babel-loader',
67
- options: {
68
- presets: ['babel-preset-expo'],
69
- plugins: ['react-native-reanimated/plugin'],
70
- },
71
- },
72
- },
73
- ...Repack.getJsTransformRules(),
74
- ]
55
+ test: /\.[jt]sx?$/,
56
+ include: [
57
+ path.resolve(dirname, 'app'),
58
+ path.resolve(dirname, 'index.js'),
59
+ /[\\/]node_modules[\\/](expo-router|react-native|@react-native|expo-modules-core|@module-federation|@react-navigation)[\\/]/
60
+ ],
61
+ resolve: {
62
+ fullySpecified: false,
63
+ }
75
64
  },
65
+ ...Repack.getJsTransformRules(),
76
66
  ...Repack.getAssetTransformRules(),
77
67
  ],
78
68
  },
@@ -83,9 +73,16 @@ function withESAD(env, options) {
83
73
  new DefinePlugin({
84
74
  'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
85
75
  '__DEV__': JSON.stringify(isDev),
76
+ 'process.env.EXPO_BASE_URL': JSON.stringify(''),
77
+ 'process.env.EXPO_OS': JSON.stringify(platform),
78
+ 'process.env.EXPO_PROJECT_ROOT': JSON.stringify(dirname),
79
+ 'process.env.EXPO_ROUTER_ABS_APP_ROOT': JSON.stringify(path.resolve(dirname, 'app')),
80
+ 'process.env.EXPO_ROUTER_APP_ROOT': JSON.stringify('~/app'),
81
+ 'process.env.EXPO_ROUTER_IMPORT_MODE': JSON.stringify('sync'),
86
82
  }),
87
83
  new ExpoModulesPlugin(),
88
84
  new Repack.RepackPlugin(),
85
+ new ReanimatedPlugin(),
89
86
  new Repack.plugins.ModuleFederationPluginV2({
90
87
  name: id,
91
88
  filename: `${id}.container.js.bundle`,
@@ -100,9 +97,11 @@ function withESAD(env, options) {
100
97
  'react-native-safe-area-context': { singleton: true, eager: true, requiredVersion: pkg.dependencies['react-native-safe-area-context'] },
101
98
  'expo-router': { singleton: true, eager: true, requiredVersion: pkg.dependencies['expo-router'] },
102
99
  'react-native-screens': { singleton: true, eager: true, requiredVersion: pkg.dependencies['react-native-screens'] },
100
+ '@module-federation/runtime': { singleton: true, eager: true },
101
+ '@module-federation/sdk': { singleton: true, eager: true },
103
102
  '@codemoreira/esad/client': {
104
103
  singleton: true,
105
- eager: options.type === 'host', // Only eager in host to ensure it's available
104
+ eager: options.type === 'host',
106
105
  import: clientPath
107
106
  },
108
107
  ...(options.shared || {})
@@ -111,24 +110,14 @@ function withESAD(env, options) {
111
110
  ],
112
111
  };
113
112
 
114
- // Add Host-specific DevServer magic for Expo
115
113
  if (options.type === 'host') {
116
114
  config.devServer = {
117
- setupMiddlewares: (middlewares) => {
118
- middlewares.unshift((req, res, next) => {
119
- if (req.url.startsWith('/.expo/.virtual-metro-entry.bundle')) {
120
- const query = req.url.split('?')[1];
121
- const isMap = req.url.includes('.map');
122
- const target = isMap ? '/index.bundle.map' : '/index.bundle';
123
- const location = query ? `${target}?${query}` : target;
124
- res.writeHead(302, { Location: location });
125
- res.end();
126
- return;
127
- }
128
- next();
129
- });
130
- return middlewares;
131
- },
115
+ proxy: [
116
+ {
117
+ context: ['/.expo/.virtual-metro-entry'],
118
+ pathRewrite: { '^/.expo/.virtual-metro-entry': '/index' },
119
+ },
120
+ ],
132
121
  };
133
122
  }
134
123