diff --git a/packages/babel-plugin/package.json b/packages/babel-plugin/package.json index 8608f6a..6d2272d 100644 --- a/packages/babel-plugin/package.json +++ b/packages/babel-plugin/package.json @@ -21,9 +21,13 @@ }, "dependencies": { "@babel/core": "^7.25.2", + "@babel/parser": "^7.26.3", + "@babel/traverse": "^7.26.4", "@rechunk/utils": "workspace:*", "chalk": "^4.0.0", - "dedent": "^1.5.3" + "dedent": "^1.5.3", + "glob": "^11.0.0", + "tsconfig-paths": "4.2.0" }, "devDependencies": { "@repo/typescript-config": "workspace:*", diff --git a/packages/babel-plugin/src/index.ts b/packages/babel-plugin/src/index.ts index a14f640..4f6a136 100644 --- a/packages/babel-plugin/src/index.ts +++ b/packages/babel-plugin/src/index.ts @@ -5,11 +5,16 @@ import { type TransformCaller, types, } from '@babel/core'; +import {parse} from '@babel/parser'; +import traverse from '@babel/traverse'; import {findClosestJSON, isRechunkDevServerRunning} from '@rechunk/utils'; import {type BabelPresetExpoOptions} from 'babel-preset-expo'; import chalk from 'chalk'; import dedent from 'dedent'; +import {readFileSync} from 'fs'; +import {glob} from 'glob'; import {relative} from 'path'; +import {loadConfig} from 'tsconfig-paths'; /** * Checks if the Babel transformation is being invoked by the ReChunk bundler. @@ -59,6 +64,11 @@ const RECHUNK_CONFIG_KEY = '@rechunk+config+json'; */ const RECHUNK_PACKAGE_KEY = '@rechunk+package+json'; +/** + * Cache key used to store and retrieve path alias imports gathered from the project. + * This key is used to cache resolved aliases to avoid re-scanning source files on subsequent runs. + */ const ALIAS_IMPORTS_CACHE_KEY = '@rechunk+alias-imports'; + /** * An array of dependencies that should be considered as extra dependencies for the project. * These dependencies might need special handling or inclusion in certain processes. @@ -72,6 +82,76 @@ const EXTRA_DEPENDENCIES = ['react/jsx-runtime']; */ const RECHUNK_DEV_SERVER_HOST = 'http://localhost:49904'; +/** + * Gathers all import statements that use path aliases from TypeScript/React files in the project. + * + * This function scans all TypeScript and React files in the project directory, analyzing their + * import statements to find those that use path aliases defined in tsconfig.json. It uses the + * project's tsconfig.json configuration to determine valid path aliases and returns a unique + * list of all aliased imports found. + * + * @param {string} projectRoot - The root directory of the project to scan for files + * @returns {string[]} An array of unique import paths that use aliases + * + * @example + * ```typescript + * // Given a file with: import Button from '@components/Button'; + * const aliases = gatherAliasImports('./project'); + * console.log(aliases); // ['@components/Button'] + * ``` + */ +function gatherAliasImports(projectRoot: string): string[] { + // Load tsconfig + const configLoaderResult = loadConfig(projectRoot); + if (configLoaderResult.resultType === 'failed') { + return []; + } + + const {paths} = configLoaderResult; + + // Create regex pattern for all aliases + const aliasPatterns = Object.keys(paths).map(alias => + alias.replace('/*', ''), + ); + const aliasRegex = new RegExp(`^(${aliasPatterns.join('|')})`); + + // Find all TypeScript/React files in the project + const sourceFiles = glob.sync('**/*.{ts,tsx}', { + ignore: ['node_modules/**', 'dist/**'], + cwd: projectRoot, + absolute: true, + }); + + const aliasImports: string[] = []; + + // Parse each file and look for imports using aliases + sourceFiles.forEach(filePath => { + try { + const code = readFileSync(filePath, 'utf-8'); + const ast = parse(code, { + sourceType: 'module', + plugins: ['typescript', 'jsx'], + }); + + traverse(ast, { + ImportDeclaration(path) { + const importSource = path.node.source.value; + + // Check if import uses an alias + if (aliasRegex.test(importSource)) { + aliasImports.push(importSource); + } + }, + }); + } catch (error) { + // Ignore parse errors for individual files + // This allows the plugin to continue gathering aliases from other files + } + }); + + return Array.from(new Set(aliasImports)); +} + export default function ( api: ConfigAPI & {types: typeof types}, ): Babel.PluginObj { @@ -422,6 +502,31 @@ export default function ( const external = rechunkJson.external || []; const dependencies = packageJson.dependencies || {}; + let aliasImports = fileCache.get(ALIAS_IMPORTS_CACHE_KEY) as string[]; + + if (!aliasImports) { + aliasImports = gatherAliasImports(process.cwd()); + fileCache.set(ALIAS_IMPORTS_CACHE_KEY, aliasImports); + } + + // Create require statements for alias imports + const aliasRequireStatements = aliasImports.map(aliasImport => + t.ifStatement( + t.binaryExpression( + '===', + t.identifier('moduleId'), + t.stringLiteral(aliasImport), + ), + t.blockStatement([ + t.returnStatement( + t.callExpression(t.identifier('require'), [ + t.stringLiteral(aliasImport), + ]), + ), + ]), + ), + ); + // Generate requireStatements for each dependency const requireStatements = [ ...Object.keys(dependencies), @@ -450,6 +555,7 @@ export default function ( [t.identifier('moduleId')], t.blockStatement([ ...requireStatements, + ...aliasRequireStatements, t.returnStatement(t.nullLiteral()), ]), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7563ae5..6867f82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -381,6 +381,12 @@ importers: '@babel/core': specifier: ^7.25.2 version: 7.25.2 + '@babel/parser': + specifier: ^7.26.3 + version: 7.26.3 + '@babel/traverse': + specifier: ^7.26.4 + version: 7.26.4 '@rechunk/utils': specifier: workspace:* version: link:../utils @@ -390,6 +396,12 @@ importers: dedent: specifier: ^1.5.3 version: 1.5.3 + glob: + specifier: ^11.0.0 + version: 11.0.0 + tsconfig-paths: + specifier: 4.2.0 + version: 4.2.0 devDependencies: '@repo/typescript-config': specifier: workspace:* @@ -14501,9 +14513,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - - bufferutil - supports-color - - utf-8-validate '@react-native/normalize-colors@0.76.5': {} diff --git a/scripts/init-local b/scripts/init-local index 46ff7ab..0d9a17c 100755 --- a/scripts/init-local +++ b/scripts/init-local @@ -15,8 +15,6 @@ # # 2. **ReChunk Initialization for the `expo` App**: # - Runs `pnpm rechunk init` with predefined host and credentials. -# - Modifies the `.rechunkrc.json` configuration file to include `"@/components"` -# in the `external` field. # - Publishes ReChunk chunks using `pnpm rechunk publish`. # # 3. **Platform Compatibility**: @@ -107,25 +105,6 @@ cd ../expo echo "Running pnpm rechunk init..." pnpm rechunk init --host http://localhost:5173/api/v1 --username admin --password password123 -# Modify .rechunkrc.json to include '@/components' in 'external' -CONFIG_FILE=".rechunkrc.json" - -# Check if the configuration file exists -if [ -f "$CONFIG_FILE" ]; then - echo "Updating 'external' field in $CONFIG_FILE..." - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS: In-place editing with an empty backup suffix - sed -i '' 's/"external": \[\(.*\)\]/"external": ["@\/components"]/' "$CONFIG_FILE" - else - # Linux: In-place editing without a backup suffix - sed -i 's/"external": \[\(.*\)\]/"external": ["@\/components"]/' "$CONFIG_FILE" - fi - echo "'external' field updated." -else - echo "Error: $CONFIG_FILE not found." - exit 1 -fi - # Publish chunks echo "Publishing chunks..." pnpm rechunk publish