Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(babel): aggregate externals #92

Merged
merged 2 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/babel-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
106 changes: 106 additions & 0 deletions packages/babel-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -450,6 +555,7 @@ export default function (
[t.identifier('moduleId')],
t.blockStatement([
...requireStatements,
...aliasRequireStatements,
t.returnStatement(t.nullLiteral()),
]),
);
Expand Down
14 changes: 12 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 0 additions & 21 deletions scripts/init-local
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand Down Expand Up @@ -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
Expand Down