Skip to content
Open
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
15 changes: 14 additions & 1 deletion packages/core/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { fileURLToPath } from 'node:url';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginSvgr } from '@rsbuild/plugin-svgr';
import { defineConfig } from '@rslib/core';
import { defineConfig, type RsbuildPlugin } from '@rslib/core';
import { pluginPublint } from 'rsbuild-plugin-publint';
import { exportStarOptimizerTransform } from './src/node/theme/exportStarOptimizerTransform';

const COMMON_EXTERNALS = [
'virtual-routes',
Expand Down Expand Up @@ -39,6 +40,7 @@ export default defineConfig({

// TODO: should add entry by new URL parser in Rspack module graph
'node/mdx/loader': './src/node/mdx/loader.ts',
'node/theme/loader': './src/node/theme/loader.ts',
'node/ssg/renderPageWorker': './src/node/ssg/renderPageWorker.ts',
},
},
Expand Down Expand Up @@ -116,6 +118,17 @@ export default defineConfig({
pluginReact(),
pluginSvgr({ svgrOptions: { exportType: 'default' } }),
pluginSass(),
{
name: 'export_star_optimizer',
setup(api) {
api.transform(
{ test: /src[\\/]theme[\\/]index\.ts/ },
({ code, resourcePath }) => {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transform function is defined inline as an async function but is not explicitly marked as async in the api.transform call. This could lead to issues if the transform is expected to return a Promise. Ensure the transform callback properly handles the async nature of exportStarOptimizerTransform by either using async/await or returning the Promise.

Suggested change
({ code, resourcePath }) => {
async ({ code, resourcePath }) => {

Copilot uses AI. Check for mistakes.
return exportStarOptimizerTransform(code, resourcePath);
},
);
},
} satisfies RsbuildPlugin,
],
source: {
define: {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/node/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const PACKAGE_ROOT = path.dirname(
require.resolve('@rspress/core/package.json'),
);
export const DEFAULT_THEME = path.join(PACKAGE_ROOT, 'dist/theme');
export const EJECTED_THEME = path.join(PACKAGE_ROOT, 'dist/eject-theme');
export const TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'index.html');

export const CSR_CLIENT_ENTRY = path.join(
Expand Down
7 changes: 2 additions & 5 deletions packages/core/src/node/eject.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { logger } from '@rspress/shared/logger';
import picocolors from 'picocolors';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import { EJECTED_THEME } from './constants';

// For testing purposes, allow overriding the theme path
let _themeComponentsPathOverride: string | undefined;
Expand All @@ -23,7 +20,7 @@ function getThemeComponentsPath(): string {
if (_themeComponentsPathOverride) {
return _themeComponentsPathOverride;
}
return path.join(__dirname, 'eject-theme/components');
return path.join(EJECTED_THEME, 'components');
}

/**
Expand Down
21 changes: 18 additions & 3 deletions packages/core/src/node/initRsbuild.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import path, { join } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath } from 'node:url';
import type {
RsbuildConfig,
Expand Down Expand Up @@ -89,7 +90,9 @@ async function createInternalBuildConfig(
routeService: RouteService,
pluginDriver: PluginDriver,
): Promise<RsbuildConfig> {
const CUSTOM_THEME_DIR = config.themeDir!;
const CUSTOM_THEME_DIR = path.isAbsolute(config.themeDir!)
? config.themeDir!
: path.join(cwd(), config.themeDir!);
const outDir = config?.outDir ?? OUTPUT_DIR;

const base = config?.base ?? '';
Expand Down Expand Up @@ -363,10 +366,22 @@ async function createInternalBuildConfig(
chain.resolve.extensions.prepend('.md').prepend('.mdx').prepend('.mjs');

chain.module
.rule('css-virtual-module')
.rule('rspress-css-virtual-module')
.test(/\.rspress[\\/]runtime[\\/]virtual-global-styles/)
.merge({ sideEffects: true });

// Optimize the theme
const themeIndexPath = join(CUSTOM_THEME_DIR, 'index');
const themeIndexRule = chain.module
.rule('rspress-theme-index')
.test(themeIndexPath);
themeIndexRule.merge({
sideEffects: false,
});
themeIndexRule
.use('EXPORT_STAR_OPTIMIZE')
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loader use name 'EXPORT_STAR_OPTIMIZE' is in SCREAMING_SNAKE_CASE while other loaders in the codebase typically use descriptive strings in kebab-case or regular naming. Consider using a more conventional name like 'export-star-optimizer' for consistency.

Suggested change
.use('EXPORT_STAR_OPTIMIZE')
.use('export-star-optimizer')

Copilot uses AI. Check for mistakes.
.loader(fileURLToPath(new URL('./theme/loader.js', import.meta.url)));

if (isSsg || isSsgMd) {
chain.optimization.splitChunks({});
}
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/node/theme/exportStarOptimizerTransform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { describe, expect, it } from '@rstest/core';
import { exportStarOptimizerTransform } from './exportStarOptimizerTransform';

const fixturesRoot = path.join(__dirname, 'fixtures');

async function loadFixture(caseName: string) {
const filePath = path.join(fixturesRoot, caseName, 'index.ts');
const code = await readFile(filePath, 'utf-8');
return { code, filePath };
}

describe('exportStarOptimizerTransform', () => {
it('replaces export * with named exports, excluding locals', async () => {
const { code, filePath } = await loadFixture('basic');
const result = await exportStarOptimizerTransform(code, filePath);

expect(result).toMatchInlineSnapshot(`
"export const A = 1;\nexport { Button, Other, default } from './foo';\n"
`);
});

it('removes export * when module exports are already local', async () => {
const { code, filePath } = await loadFixture('local-only');
const result = await exportStarOptimizerTransform(code, filePath);

expect(result).toMatchInlineSnapshot(`
"export const A = 1;

"
`);
});

it('resolves index fallback when no direct file match', async () => {
const { code, filePath } = await loadFixture('index-fallback');
const result = await exportStarOptimizerTransform(code, filePath);

expect(result).toMatchInlineSnapshot(`
"export { B } from './foo';\n"
`);
});
});
223 changes: 223 additions & 0 deletions packages/core/src/node/theme/exportStarOptimizerTransform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { readFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { rspack } from '@rsbuild/core';

type ResolveAsync = (
directory: string,
request: string,
) => Promise<{ path?: string; error?: string }>;

const resolver = new rspack.experiments.resolver.ResolverFactory({
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx', '.json'],
mainFiles: ['index'],
mainFields: ['module', 'browser', 'main'],
alias: {},
});

/**
* Convert `export *` into named exports via regex for tree-shaking friendliness.
*/
class ExportStarOptimizer {
filepath: string;
_resolve: ResolveAsync | undefined;

localExports: Set<string>;

reExports: Map<string, { source: string; imported: string }>;

constructor(filepath: string, resolveAsync?: ResolveAsync) {
this.filepath = filepath;
this.localExports = new Set();
this.reExports = new Map();
this._resolve = resolveAsync;
}

resolve(
directory: string,
request: string,
): Promise<{ path?: string; error?: string }> {
if (this._resolve) {
return this._resolve(directory, request);
}
return resolver.async(directory, request);
}

/**
* Parse source code to collect local exports.
*/
parseLocalExports(code: string): void {
// 移除注释
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chinese comment in code. Consider translating to English for consistency with the rest of the codebase. The comment reads "移除注释" which means "Remove comments".

Suggested change
// 移除注释
// Remove comments

Copilot uses AI. Check for mistakes.
const cleanCode = this.removeComments(code);

// 1. export const/let/var name = ...
const varExportRegex = /export\s+(?:const|let|var)\s+(\w+)/g;
let match: RegExpExecArray | null;
while ((match = varExportRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
Comment on lines +53 to +56
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex /export\s+(?:const|let|var)\s+(\w+)/g only captures the first identifier in a multi-variable declaration. For example, 'export const a = 1, b = 2, c = 3;' would only capture 'a', missing 'b' and 'c'. Consider extending the regex or parsing logic to handle comma-separated variable declarations.

Suggested change
const varExportRegex = /export\s+(?:const|let|var)\s+(\w+)/g;
let match: RegExpExecArray | null;
while ((match = varExportRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
// Match the whole declaration after export const|let|var, up to semicolon or line break
const varExportRegex = /export\s+(?:const|let|var)\s+([^;]+)/g;
let match: RegExpExecArray | null;
while ((match = varExportRegex.exec(cleanCode)) !== null) {
// Split by commas, handle each variable declaration
const declarations = match[1].split(',');
for (const decl of declarations) {
// Extract variable name (ignore destructuring for now)
const varNameMatch = /^\s*([A-Za-z_$][\w$]*)/.exec(decl.trim());
if (varNameMatch) {
this.localExports.add(varNameMatch[1]);
}
}

Copilot uses AI. Check for mistakes.
}

// 2. export function name() {} 或 export class Name {}
const funcClassRegex = /export\s+(?:function|class)\s+(\w+)/g;
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex /export\s+(?:function|class)\s+(\w+)/g doesn't handle async functions ('export async function'), generator functions ('export function*'), or abstract classes ('export abstract class'). These TypeScript and ES2015+ constructs would be missed by this pattern. Consider extending the regex to handle these cases.

Suggested change
const funcClassRegex = /export\s+(?:function|class)\s+(\w+)/g;
// Handles: export function name, export async function name, export function* name, export async function* name,
// export class Name, export abstract class Name
const funcClassRegex = /export\s+(?:(?:async\s+)?function\s*\*?|(?:abstract\s+)?class)\s+(\w+)/g;

Copilot uses AI. Check for mistakes.
while ((match = funcClassRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
}

// 3. export { name1, name2 }
const namedExportRegex = /export\s*\{([^}]+)\}/g;
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex for parsing named exports doesn't account for 'export { name } from' statements, which would be incorrectly parsed as local exports. The regex should use a negative lookahead to exclude re-exports: /export\s*{([^}]+)}(?!\s*from)/g to avoid false positives.

Suggested change
const namedExportRegex = /export\s*\{([^}]+)\}/g;
const namedExportRegex = /export\s*\{([^}]+)\}(?!\s*from)/g;

Copilot uses AI. Check for mistakes.
while ((match = namedExportRegex.exec(cleanCode)) !== null) {
const names = match[1].split(',').map(n => {
// Handle `name as alias`.
const parts = n.trim().split(/\s+as\s+/);
return parts[parts.length - 1].trim();
});
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parsing logic for 'export { name as alias }' is incorrect. When splitting by 'as', parts[parts.length - 1] gets the alias (exported name), but for local exports in the parseLocalExports method, we should track the alias, not the original name. However, this logic seems backwards - it should store the exported name (after 'as'), which is correct. But the logic for adding names from lines 49 doesn't properly validate empty strings after trim(), which could add empty entries to localExports if there are trailing commas or extra spaces.

Suggested change
});
}).filter(name => name.length > 0);

Copilot uses AI. Check for mistakes.
names.forEach(name => this.localExports.add(name));
}

// 4. export { name } from 'module'
const reExportRegex = /export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
while ((match = reExportRegex.exec(cleanCode)) !== null) {
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/));
const source = match[2];
names.forEach(parts => {
const exported = parts[parts.length - 1].trim();
const imported = parts[0].trim();
this.localExports.add(exported);
this.reExports.set(exported, { source, imported });
});
}

// 5. export default
if (/export\s+default/.test(cleanCode)) {
this.localExports.add('default');
}
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parseLocalExports method doesn't handle TypeScript-specific exports such as 'export type', 'export interface', 'export enum', 'export namespace', and 'export declare'. These are valid exports in TypeScript files and should be tracked to avoid conflicts when expanding 'export *' statements. Since the file extension list in getModuleExports includes '.ts' and '.tsx', TypeScript support should be comprehensive.

Suggested change
}
}
// 6. TypeScript-specific exports
// export type Name = ...
const typeExportRegex = /export\s+type\s+(\w+)/g;
while ((match = typeExportRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
}
// export interface Name { ... }
const interfaceExportRegex = /export\s+interface\s+(\w+)/g;
while ((match = interfaceExportRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
}
// export enum Name { ... }
const enumExportRegex = /export\s+enum\s+(\w+)/g;
while ((match = enumExportRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
}
// export namespace Name { ... }
const namespaceExportRegex = /export\s+namespace\s+(\w+)/g;
while ((match = namespaceExportRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
}
// export declare ... (type, interface, enum, namespace, function, class, const, let, var)
const declareExportRegex = /export\s+declare\s+(?:type|interface|enum|namespace|function|class|const|let|var)\s+(\w+)/g;
while ((match = declareExportRegex.exec(cleanCode)) !== null) {
this.localExports.add(match[1]);
}

Copilot uses AI. Check for mistakes.
}

/**
* Get all exports of a module.
*/
async getModuleExports(modulePath: string): Promise<Set<string>> {
const baseDir = dirname(this.filepath);
const resolved = await this.resolve(baseDir, modulePath);

if (resolved.error || !resolved.path) {
throw new Error(resolved.error || 'Failed to resolve path');
}

try {
const moduleCode = await readFile(resolved.path, 'utf-8');
const cleanCode = this.removeComments(moduleCode);
const exports = new Set<string>();

// Extract export names.
const varExportRegex = /export\s+(?:const|let|var)\s+(\w+)/g;
let match: RegExpExecArray | null;
while ((match = varExportRegex.exec(cleanCode)) !== null) {
exports.add(match[1]);
Comment on lines +112 to +115
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same regex bug exists in getModuleExports: /export\s+(?:const|let|var)\s+(\w+)/g only captures the first variable in multi-variable declarations like 'export const a = 1, b = 2;'. This inconsistency with parseLocalExports should be fixed to handle comma-separated declarations.

Suggested change
const varExportRegex = /export\s+(?:const|let|var)\s+(\w+)/g;
let match: RegExpExecArray | null;
while ((match = varExportRegex.exec(cleanCode)) !== null) {
exports.add(match[1]);
const varExportRegex = /export\s+(?:const|let|var)\s+([^;]+)/g;
let match: RegExpExecArray | null;
while ((match = varExportRegex.exec(cleanCode)) !== null) {
// match[1] contains the full variable declaration list, e.g. "a = 1, b = 2"
const varList = match[1];
// Split by commas not inside brackets (to avoid destructuring confusion)
varList.split(',').forEach(decl => {
// Remove any default value assignment and destructuring
// Only match simple variable names (skip destructuring for now)
const nameMatch = /^\s*([a-zA-Z_$][\w$]*)/.exec(decl.trim());
if (nameMatch) {
exports.add(nameMatch[1]);
}
});

Copilot uses AI. Check for mistakes.
}

const funcClassRegex = /export\s+(?:function|class)\s+(\w+)/g;
while ((match = funcClassRegex.exec(cleanCode)) !== null) {
exports.add(match[1]);
}

const namedExportRegex = /export\s*\{([^}]+)\}(?!\s*from)/g;
while ((match = namedExportRegex.exec(cleanCode)) !== null) {
const names = match[1].split(',').map(n => {
const parts = n.trim().split(/\s+as\s+/);
return parts[parts.length - 1].trim();
});
names.forEach(name => exports.add(name));
}

Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to parseLocalExports, getModuleExports doesn't handle TypeScript-specific exports like 'export type', 'export interface', 'export enum', etc. This could lead to incomplete export lists when processing TypeScript modules, potentially causing missing exports in the optimized output.

Suggested change
// TypeScript-specific: export type Foo, export interface Bar, export enum Baz
const typeExportRegex = /export\s+type\s+(\w+)/g;
while ((match = typeExportRegex.exec(cleanCode)) !== null) {
exports.add(match[1]);
}
const interfaceExportRegex = /export\s+interface\s+(\w+)/g;
while ((match = interfaceExportRegex.exec(cleanCode)) !== null) {
exports.add(match[1]);
}
const enumExportRegex = /export\s+enum\s+(\w+)/g;
while ((match = enumExportRegex.exec(cleanCode)) !== null) {
exports.add(match[1]);
}

Copilot uses AI. Check for mistakes.
const reExportRegex = /export\s*\{([^}]+)\}\s*from/g;
while ((match = reExportRegex.exec(cleanCode)) !== null) {
const names = match[1].split(',').map(n => {
const parts = n.trim().split(/\s+as\s+/);
return parts[parts.length - 1].trim();
});
names.forEach(name => exports.add(name));
}
Comment on lines +125 to +139
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for parsing export names from 'export { ... }' statements (lines 108-112) is duplicated with nearly identical logic at lines 117-121. The only difference is whether they filter for 'from' clauses. Consider combining these into a single parsing step with appropriate filtering to reduce duplication.

Copilot uses AI. Check for mistakes.

if (/export\s+default/.test(cleanCode)) {
exports.add('default');
}
Comment on lines +111 to +143
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The export parsing logic in getModuleExports (lines 95-126) is nearly identical to the logic in parseLocalExports (lines 28-68). This represents significant code duplication. Consider extracting this logic into a shared private method to improve maintainability and ensure consistency.

Copilot uses AI. Check for mistakes.

return exports;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(`Cannot parse module ${modulePath}: ${message}`);
return new Set();
}
Comment on lines 146 to 150
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When module resolution fails, the method returns an empty Set and only logs a warning. This silently causes the 'export *' statement to be replaced with nothing, which changes the semantic behavior of the code. If a module genuinely exists but fails to resolve, this could break the application. Consider either preserving the original 'export *' statement on resolution failure, or making the error more visible (e.g., throwing an error in non-production builds).

Copilot uses AI. Check for mistakes.
}

/**
* Remove comments from code.
*/
removeComments(code: string): string {
// Remove single-line comments
code = code.replace(/\/\/.*$/gm, '');
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex for removing single-line comments will incorrectly remove URLs and strings that contain '//', such as 'https://example.com' or strings like 'const url = "http://foo"'. This could corrupt valid code. Consider using a more robust comment removal approach that respects string literals and template literals, or use a proper AST-based parser.

Copilot uses AI. Check for mistakes.
// Remove multi-line comments
code = code.replace(/\/\*[\s\S]*?\*\//g, '');
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex for removing multi-line comments could incorrectly match '/' and '/' patterns that appear in strings or regular expressions. For example, code like 'const pattern = /*/' or 'const str = "/* not a comment */"' would be corrupted. Consider using an AST-based approach for more accurate comment removal.

Copilot uses AI. Check for mistakes.
return code;
}

/**
* Transform code by replacing `export *`.
*/
async transform(code: string): Promise<string> {
// Parse local exports.
this.parseLocalExports(code);

// Find all `export * from 'xxx'` statements.
const exportStarRegex = /export\s*\*\s*from\s*['"]([^'"]+)['"]\s*;?/g;
let match: RegExpExecArray | null;
const replacements: Array<{
startIndex: number;
endIndex: number;
replacement: string;
}> = [];

while ((match = exportStarRegex.exec(code)) !== null) {
const fullMatch = match[0];
const source = match[1];
const startIndex = match.index;
const endIndex = startIndex + fullMatch.length;

// Get exports of the target module.
const moduleExports = await this.getModuleExports(source);
Comment on lines +180 to +187
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each 'export *' statement triggers an async module resolution and file read operation in getModuleExports. If a file has multiple 'export *' statements referencing the same module, this module will be resolved and read multiple times. Consider caching the results of getModuleExports by module path to avoid redundant I/O operations.

Suggested change
while ((match = exportStarRegex.exec(code)) !== null) {
const fullMatch = match[0];
const source = match[1];
const startIndex = match.index;
const endIndex = startIndex + fullMatch.length;
// Get exports of the target module.
const moduleExports = await this.getModuleExports(source);
// Cache to avoid redundant module resolution and file reads.
const moduleExportsCache = new Map<string, Set<string>>();
while ((match = exportStarRegex.exec(code)) !== null) {
const fullMatch = match[0];
const source = match[1];
const startIndex = match.index;
const endIndex = startIndex + fullMatch.length;
// Get exports of the target module, using cache if available.
let moduleExports: Set<string>;
if (moduleExportsCache.has(source)) {
moduleExports = moduleExportsCache.get(source)!;
} else {
moduleExports = await this.getModuleExports(source);
moduleExportsCache.set(source, moduleExports);
}

Copilot uses AI. Check for mistakes.

// Filter out exports already defined locally (avoid conflicts).
const exportsToInclude = Array.from(moduleExports)
.filter(name => !this.localExports.has(name))
.sort();

// Generate the replacement export statement.
let replacement = '';
if (exportsToInclude.length > 0) {
replacement = `export { ${exportsToInclude.join(', ')} } from '${source}';`;
}

replacements.push({ startIndex, endIndex, replacement });
}

// Replace from back to front to avoid index shifts.
replacements.reverse().forEach(({ startIndex, endIndex, replacement }) => {
code =
code.substring(0, startIndex) + replacement + code.substring(endIndex);
});

return code;
}
}

/**
* Transform a code string.
*/
export async function exportStarOptimizerTransform(
code: string,
filepath: string,
resolveAsync?: ResolveAsync,
): Promise<string> {
const transformer = new ExportStarOptimizer(filepath, resolveAsync);
return transformer.transform(code);
}
3 changes: 3 additions & 0 deletions packages/core/src/node/theme/fixtures/basic/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const Button = 1;
export default 2;
export const Other = 3;
2 changes: 2 additions & 0 deletions packages/core/src/node/theme/fixtures/basic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const A = 1;
export * from './foo';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const B = 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './foo';
1 change: 1 addition & 0 deletions packages/core/src/node/theme/fixtures/local-only/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const A = 1;
2 changes: 2 additions & 0 deletions packages/core/src/node/theme/fixtures/local-only/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const A = 1;
export * from './foo';
Loading
Loading