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(turbopack): add support for bundlePagesRouterDependencies #65520

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 17 additions & 3 deletions packages/next-swc/crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ pub struct NextConfig {
pub dev_indicators: Option<DevIndicatorsConfig>,
pub output: Option<OutputType>,

/// Enables the bundling of node_modules packages (externals) for pages
/// server-side bundles.
///
/// [API Reference](https://nextjs.org/docs/pages/api-reference/next-config-js/bundlePagesRouterDependencies)
pub bundle_pages_router_dependencies: Option<bool>,

/// A list of packages that should be treated as external on the server
/// build.
///
/// [API Reference](https://nextjs.org/docs/app/api-reference/next-config-js/serverExternalPackages)
pub server_external_packages: Option<Vec<String>>,

#[serde(rename = "_originalRedirects")]
pub original_redirects: Option<Vec<Redirect>>,

Expand Down Expand Up @@ -120,9 +132,6 @@ pub struct NextConfig {
typescript: TypeScriptConfig,
use_file_system_public_routes: bool,
webpack: Option<serde_json::Value>,
/// A list of packages that should be treated as external in the RSC server
/// build. @see [api reference](https://nextjs.org/docs/app/api-reference/next-config-js/server_external_packages)
pub server_external_packages: Option<Vec<String>>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)]
Expand Down Expand Up @@ -736,6 +745,11 @@ impl NextConfig {
Ok(config.cell())
}

#[turbo_tasks::function]
pub fn bundle_pages_router_dependencies(&self) -> Vc<bool> {
Vc::cell(self.bundle_pages_router_dependencies.unwrap_or_default())
}

#[turbo_tasks::function]
pub async fn server_external_packages(self: Vc<Self>) -> Result<Vc<Vec<String>>> {
Ok(Vc::cell(
Expand Down
49 changes: 35 additions & 14 deletions packages/next-swc/crates/next-core/src/next_server/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::iter::once;

use anyhow::Result;
use anyhow::{bail, Result};
use indexmap::IndexMap;
use turbo_tasks::{Value, Vc};
use turbo_tasks_fs::FileSystem;
Expand Down Expand Up @@ -128,22 +128,39 @@ pub async fn get_server_resolve_options_context(
let invalid_styled_jsx_client_only_resolve_plugin =
get_invalid_styled_jsx_resolve_plugin(project_path);

let mut transpile_packages = next_config.transpile_packages().await?.clone_value();
transpile_packages.extend(
(*next_config.optimize_package_imports().await?)
.iter()
.cloned(),
);

// Always load these predefined packages as external.
let mut external_packages: Vec<String> = load_next_js_templateon(
project_path,
"dist/lib/server-external-packages.json".to_string(),
)
.await?;

let transpile_packages = next_config.transpile_packages().await?;
external_packages.retain(|item| !transpile_packages.contains(item));
let server_external_packages = &*next_config.server_external_packages().await?;

let conflicting_packages = transpile_packages
.iter()
.filter(|package| server_external_packages.contains(package))
.collect::<Vec<_>>();

if !conflicting_packages.is_empty() {
bail!(
"The packages specified in the 'transpilePackages' conflict with the \
'serverExternalPackages': {:?}",
conflicting_packages
);
}

// Add the config's own list of external packages.
external_packages.extend(
(*next_config.server_external_packages().await?)
.iter()
.cloned(),
);
external_packages.extend(server_external_packages.iter().cloned());

external_packages.retain(|item| !transpile_packages.contains(item));

let server_external_packages_plugin = ExternalCjsModulesResolvePlugin::new(
project_path,
Expand All @@ -166,12 +183,16 @@ pub async fn get_server_resolve_options_context(
custom_conditions.push("react-server".to_string());
};

let external_cjs_modules_plugin = ExternalCjsModulesResolvePlugin::new(
project_path,
project_path.root(),
ExternalPredicate::AllExcept(next_config.transpile_packages()).cell(),
*next_config.import_externals().await?,
);
let external_cjs_modules_plugin = if *next_config.bundle_pages_router_dependencies().await? {
server_external_packages_plugin
} else {
ExternalCjsModulesResolvePlugin::new(
project_path,
project_path.root(),
ExternalPredicate::AllExcept(Vc::cell(transpile_packages)).cell(),
*next_config.import_externals().await?,
)
};

let next_external_plugin = NextExternalResolvePlugin::new(project_path);
let next_node_shared_runtime_plugin =
Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/build/handle-externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function makeExternalHandler({
// Absolute requires (require('/foo')) are extremely uncommon, but
// also have no need for customization as they're already resolved.
if (!isLocal) {
if (/^(?:next$)/.test(request)) {
if (/^next$/.test(request)) {
return `commonjs ${request}`
}

Expand Down Expand Up @@ -380,13 +380,15 @@ function resolveBundlingOptOutPackages({
if (nodeModulesRegex.test(resolvedRes)) {
const shouldBundlePages =
!isAppLayer && config.bundlePagesRouterDependencies && !isOptOutBundling

const shouldBeBundled =
shouldBundlePages ||
isResourceInPackages(
resolvedRes,
config.transpilePackages,
resolvedExternalPackageDirs
)

if (!shouldBeBundled) {
return `${externalType} ${request}` // Externalize if not bundled or opted out
}
Expand Down
11 changes: 6 additions & 5 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,11 +836,12 @@ export interface NextConfig extends Record<string, any> {

/**
* Enables the bundling of node_modules packages (externals) for pages server-side bundles.
* @see https://nextjs.org/docs/pages/api-reference/next-config-js/bundlePagesRouterDependencies
*/
bundlePagesRouterDependencies?: boolean

/**
* A list of packages that should be treated as external in the RSC server build.
* A list of packages that should be treated as external in the server build.
* @see https://nextjs.org/docs/app/api-reference/next-config-js/serverExternalPackages
*/
serverExternalPackages?: string[]
Expand Down Expand Up @@ -955,10 +956,10 @@ export const defaultConfig: NextConfig = {
// If we're testing, and the `__NEXT_EXPERIMENTAL_PPR` environment variable
// has been set to `true`, enable the experimental PPR feature so long as it
// wasn't explicitly disabled in the config.
process.env.__NEXT_TEST_MODE &&
process.env.__NEXT_EXPERIMENTAL_PPR === 'true'
? true
: false,
!!(
process.env.__NEXT_TEST_MODE &&
process.env.__NEXT_EXPERIMENTAL_PPR === 'true'
),
webpackBuildWorker: undefined,
missingSuspenseWithCSRBailout: true,
optimizeServerReact: true,
Expand Down
55 changes: 44 additions & 11 deletions test/integration/externals-pages-bundle/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,55 @@ describe('bundle pages externals with config.bundlePagesRouterDependencies', ()
'production mode',
() => {
beforeAll(async () => {
await nextBuild(appDir, [], { stdout: true })
await nextBuild(appDir, [])
})

it('should have no externals with the config set', async () => {
const output = await fs.readFile(
join(appDir, '.next/server/pages/index.js'),
'utf8'
)
expect(output).not.toContain('require("external-package")')
if (process.env.TURBOPACK) {
const ssrPath = join(appDir, '.next/server/chunks/ssr')
const pageBundleBasenames = (await fs.readdir(ssrPath)).filter((p) =>
p.match(/\.js$/)
)
expect(pageBundleBasenames).not.toBeEmpty()
let allBundles = ''
for (const basename of pageBundleBasenames) {
const output = await fs.readFile(join(ssrPath, basename), 'utf8')
allBundles += output
}

// we don't know the name of the minified `__turbopack_external_require__`, so we just check the arguments.
expect(allBundles).not.toContain('("external-package",!0)')
} else {
const output = await fs.readFile(
join(appDir, '.next/server/pages/index.js'),
'utf8'
)
expect(output).not.toContain('require("external-package")')
}
})

it('should respect the serverExternalPackages config', async () => {
const output = await fs.readFile(
join(appDir, '.next/server/pages/index.js'),
'utf8'
)
expect(output).toContain('require("opted-out-external-package")')
if (process.env.TURBOPACK) {
const ssrPath = join(appDir, '.next/server/chunks/ssr')
const pageBundleBasenames = (await fs.readdir(ssrPath)).filter((p) =>
p.match(/\.js$/)
)
expect(pageBundleBasenames).not.toBeEmpty()
let allBundles = ''
for (const basename of pageBundleBasenames) {
const output = await fs.readFile(join(ssrPath, basename), 'utf8')
allBundles += output
}

// we don't know the name of the minified `__turbopack_external_require__`, so we just check the arguments.
expect(allBundles).toContain('("opted-out-external-package",!0)')
} else {
const output = await fs.readFile(
join(appDir, '.next/server/pages/index.js'),
'utf8'
)
expect(output).toContain('require("opted-out-external-package")')
}
})
}
)
Expand Down
4 changes: 2 additions & 2 deletions test/turbopack-build-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8483,11 +8483,11 @@
"runtimeError": false
},
"test/integration/externals-pages-bundle/test/index.test.js": {
"passed": [],
"failed": [
"passed": [
"bundle pages externals with config.bundlePagesRouterDependencies production mode should have no externals with the config set",
"bundle pages externals with config.bundlePagesRouterDependencies production mode should respect the serverExternalPackages config"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
Expand Down