refactor: Add lightningcss mode for turbopack-css (#58471)

### What?

We are experimenting with `lightningcss`. This is about replacing
`swc_css` with `lightningcss` in turbopack, and the main reason for this
is to reduce the maintenance burden.
But when I tried, it introduced several regressions, so I'm putting it
behind an experimental flag.

You can enable `lightningcss` mode for **turbopack** by adding a flag to
the next config file.

```js
/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  experimental: {
    useLightningcss: true,
  },
}

module.exports = nextConfig

```

Note that this is only for turbopack because we were not using `swc_css`
for non-turbopack mode of next.js



x-ref:
https://vercel.slack.com/archives/C03EWR7LGEN/p1700025496732229?thread_ts=1700019629.866549&cid=C03EWR7LGEN


### Why?

We should avoid regressions.

### How?

---

turbopack PR: https://github.com/vercel/turbo/pull/6456


Closes PACK-1966

---------

Co-authored-by: Leah <github.leah@hrmny.sh>
This commit is contained in:
Donny/강동윤 2023-11-21 02:09:36 +09:00 committed by GitHub
parent 32c9ce6805
commit 94771bf3cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 347 additions and 215 deletions

405
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -36,18 +36,18 @@ next-transform-dynamic = { path = "packages/next-swc/crates/next-transform-dynam
next-transform-strip-page-exports = { path = "packages/next-swc/crates/next-transform-strip-page-exports" } next-transform-strip-page-exports = { path = "packages/next-swc/crates/next-transform-strip-page-exports" }
# SWC crates # SWC crates
swc_core = { version = "0.86.40", features = [ swc_core = { version = "0.86.60", features = [
"ecma_loader_lru", "ecma_loader_lru",
"ecma_loader_parking_lot", "ecma_loader_parking_lot",
] } ] }
testing = { version = "0.35.7" } testing = { version = "0.35.10" }
# Turbo crates # Turbo crates
turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-231117.4" } turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-231120.2" }
# [TODO]: need to refactor embed_directory! macro usages, as well as resolving turbo_tasks::function, macros.. # [TODO]: need to refactor embed_directory! macro usages, as well as resolving turbo_tasks::function, macros..
turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-231117.4" } turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-231120.2" }
# [TODO]: need to refactor embed_directory! macro usage in next-core # [TODO]: need to refactor embed_directory! macro usage in next-core
turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-231117.4" } turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-231120.2" }
# General Deps # General Deps
@ -135,3 +135,4 @@ url = "2.2.2"
urlencoding = "2.1.2" urlencoding = "2.1.2"
webbrowser = "0.8.7" webbrowser = "0.8.7"
dhat = { version = "0.3.2" } dhat = { version = "0.3.2" }

View file

@ -38,8 +38,8 @@ turbopack-binding = { workspace = true, features = [
"__swc_transform_modularize_imports", "__swc_transform_modularize_imports",
"__swc_transform_relay", "__swc_transform_relay",
] } ] }
react_remove_properties = "0.10.0" react_remove_properties = "0.11.0"
remove_console = "0.11.0" remove_console = "0.12.0"
[dev-dependencies] [dev-dependencies]
turbopack-binding = { workspace = true, features = [ turbopack-binding = { workspace = true, features = [

View file

@ -237,12 +237,14 @@ pub async fn get_client_module_options_context(
.cell() .cell()
}); });
let use_lightningcss = *next_config.use_lightningcss().await?;
let source_transforms = vec![ let source_transforms = vec![
*get_swc_ecma_transform_plugin(project_path, next_config).await?, *get_swc_ecma_transform_plugin(project_path, next_config).await?,
*get_relay_transform_plugin(next_config).await?, *get_relay_transform_plugin(next_config).await?,
*get_emotion_transform_plugin(next_config).await?, *get_emotion_transform_plugin(next_config).await?,
*get_styled_components_transform_plugin(next_config).await?, *get_styled_components_transform_plugin(next_config).await?,
*get_styled_jsx_transform_plugin().await?, *get_styled_jsx_transform_plugin(use_lightningcss).await?,
] ]
.into_iter() .into_iter()
.flatten() .flatten()
@ -302,6 +304,7 @@ pub async fn get_client_module_options_context(
), ),
], ],
custom_rules, custom_rules,
use_lightningcss,
..module_options_context ..module_options_context
} }
.cell(); .cell();

View file

@ -3,10 +3,14 @@ use turbo_tasks::Vc;
use turbopack_binding::turbopack::{ use turbopack_binding::turbopack::{
core::{ core::{
asset::{Asset, AssetContent}, asset::{Asset, AssetContent},
chunk::ChunkingContext,
ident::AssetIdent, ident::AssetIdent,
module::Module, module::Module,
}, },
turbopack::css::{chunk::CssChunkPlaceable, ParseCss, ParseCssResult}, turbopack::css::{
chunk::CssChunkPlaceable, CssWithPlaceholderResult, FinalCssResult, ParseCss,
ParseCssResult, ProcessCss,
},
}; };
/// A [`CssClientReferenceModule`] is a marker module used to indicate which /// A [`CssClientReferenceModule`] is a marker module used to indicate which
@ -63,3 +67,29 @@ impl ParseCss for CssClientReferenceModule {
Ok(parse_css.parse_css()) Ok(parse_css.parse_css())
} }
} }
#[turbo_tasks::value_impl]
impl ProcessCss for CssClientReferenceModule {
#[turbo_tasks::function]
async fn get_css_with_placeholder(&self) -> Result<Vc<CssWithPlaceholderResult>> {
let Some(imp) = Vc::try_resolve_sidecast::<Box<dyn ProcessCss>>(self.client_module).await?
else {
bail!("CSS client reference client module must be CSS processable");
};
Ok(imp.get_css_with_placeholder())
}
#[turbo_tasks::function]
async fn finalize_css(
&self,
chunking_context: Vc<Box<dyn ChunkingContext>>,
) -> Result<Vc<FinalCssResult>> {
let Some(imp) = Vc::try_resolve_sidecast::<Box<dyn ProcessCss>>(self.client_module).await?
else {
bail!("CSS client reference client module must be CSS processable");
};
Ok(imp.finalize_css(chunking_context))
}
}

View file

@ -503,6 +503,8 @@ pub struct ExperimentalConfig {
/// (doesn't apply to Turbopack). /// (doesn't apply to Turbopack).
webpack_build_worker: Option<bool>, webpack_build_worker: Option<bool>,
worker_threads: Option<bool>, worker_threads: Option<bool>,
use_lightningcss: Option<bool>,
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)]
@ -765,6 +767,13 @@ impl NextConfig {
pub async fn enable_taint(self: Vc<Self>) -> Result<Vc<bool>> { pub async fn enable_taint(self: Vc<Self>) -> Result<Vc<bool>> {
Ok(Vc::cell(self.await?.experimental.taint.unwrap_or(false))) Ok(Vc::cell(self.await?.experimental.taint.unwrap_or(false)))
} }
#[turbo_tasks::function]
pub async fn use_lightningcss(self: Vc<Self>) -> Result<Vc<bool>> {
Ok(Vc::cell(
self.await?.experimental.use_lightningcss.unwrap_or(false),
))
}
} }
fn next_configs() -> Vc<Vec<String>> { fn next_configs() -> Vc<Vec<String>> {

View file

@ -291,10 +291,12 @@ pub async fn get_server_module_options_context(
.cell() .cell()
}); });
let use_lightningcss = *next_config.use_lightningcss().await?;
// EcmascriptTransformPlugins for custom transforms // EcmascriptTransformPlugins for custom transforms
let styled_components_transform_plugin = let styled_components_transform_plugin =
*get_styled_components_transform_plugin(next_config).await?; *get_styled_components_transform_plugin(next_config).await?;
let styled_jsx_transform_plugin = *get_styled_jsx_transform_plugin().await?; let styled_jsx_transform_plugin = *get_styled_jsx_transform_plugin(use_lightningcss).await?;
// ModuleOptionsContext related options // ModuleOptionsContext related options
let tsconfig = get_typescript_transform_options(project_path); let tsconfig = get_typescript_transform_options(project_path);
@ -363,6 +365,7 @@ pub async fn get_server_module_options_context(
let module_options_context = ModuleOptionsContext { let module_options_context = ModuleOptionsContext {
execution_context: Some(execution_context), execution_context: Some(execution_context),
esm_url_rewrite_behavior: url_rewrite_behavior, esm_url_rewrite_behavior: url_rewrite_behavior,
use_lightningcss,
..Default::default() ..Default::default()
}; };
@ -431,6 +434,7 @@ pub async fn get_server_module_options_context(
let module_options_context = ModuleOptionsContext { let module_options_context = ModuleOptionsContext {
custom_ecma_transform_plugins: base_ecma_transform_plugins, custom_ecma_transform_plugins: base_ecma_transform_plugins,
execution_context: Some(execution_context), execution_context: Some(execution_context),
use_lightningcss,
..Default::default() ..Default::default()
}; };
let foreign_code_module_options_context = ModuleOptionsContext { let foreign_code_module_options_context = ModuleOptionsContext {
@ -515,6 +519,7 @@ pub async fn get_server_module_options_context(
let module_options_context = ModuleOptionsContext { let module_options_context = ModuleOptionsContext {
custom_ecma_transform_plugins: base_ecma_transform_plugins, custom_ecma_transform_plugins: base_ecma_transform_plugins,
execution_context: Some(execution_context), execution_context: Some(execution_context),
use_lightningcss,
..Default::default() ..Default::default()
}; };
let foreign_code_module_options_context = ModuleOptionsContext { let foreign_code_module_options_context = ModuleOptionsContext {

View file

@ -7,8 +7,10 @@ use turbopack_binding::turbopack::{
/// Returns a transform plugin for the relay graphql transform. /// Returns a transform plugin for the relay graphql transform.
#[turbo_tasks::function] #[turbo_tasks::function]
pub async fn get_styled_jsx_transform_plugin() -> Result<Vc<OptionTransformPlugin>> { pub async fn get_styled_jsx_transform_plugin(
use_lightningcss: bool,
) -> Result<Vc<OptionTransformPlugin>> {
Ok(Vc::cell(Some(Vc::cell( Ok(Vc::cell(Some(Vc::cell(
Box::new(StyledJsxTransformer::new()) as _, Box::new(StyledJsxTransformer::new(use_lightningcss)) as _,
)))) ))))
} }

View file

@ -193,7 +193,7 @@
"@types/ws": "8.2.0", "@types/ws": "8.2.0",
"@vercel/ncc": "0.34.0", "@vercel/ncc": "0.34.0",
"@vercel/nft": "0.22.6", "@vercel/nft": "0.22.6",
"@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231113.3", "@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231120.2",
"acorn": "8.5.0", "acorn": "8.5.0",
"amphtml-validator": "1.0.35", "amphtml-validator": "1.0.35",
"anser": "1.4.9", "anser": "1.4.9",

View file

@ -66,6 +66,7 @@ const supportedTurbopackNextConfigOptions = [
'experimental.useDeploymentId', 'experimental.useDeploymentId',
'experimental.useDeploymentIdServerActions', 'experimental.useDeploymentIdServerActions',
'experimental.deploymentId', 'experimental.deploymentId',
'experimental.useLightningcss',
// Experimental options that don't affect compilation // Experimental options that don't affect compilation
'experimental.ppr', 'experimental.ppr',

View file

@ -368,6 +368,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
bundlePagesExternals: z.boolean().optional(), bundlePagesExternals: z.boolean().optional(),
staticWorkerRequestDeduping: z.boolean().optional(), staticWorkerRequestDeduping: z.boolean().optional(),
useWasmBinary: z.boolean().optional(), useWasmBinary: z.boolean().optional(),
useLightningcss: z.boolean().optional(),
}) })
.optional(), .optional(),
exportPathMap: z exportPathMap: z

View file

@ -342,6 +342,11 @@ export interface ExperimentalConfig {
staticWorkerRequestDeduping?: boolean staticWorkerRequestDeduping?: boolean
useWasmBinary?: boolean useWasmBinary?: boolean
/**
* Use lightningcss instead of swc_css
*/
useLightningcss?: boolean
} }
export type ExportPathMap = { export type ExportPathMap = {

View file

@ -1065,8 +1065,8 @@ importers:
specifier: 0.22.6 specifier: 0.22.6
version: 0.22.6 version: 0.22.6
'@vercel/turbopack-ecmascript-runtime': '@vercel/turbopack-ecmascript-runtime':
specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231113.3 specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231120.2
version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231113.3(react-refresh@0.12.0)(webpack@5.86.0)' version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231120.2(react-refresh@0.12.0)(webpack@5.86.0)'
acorn: acorn:
specifier: 8.5.0 specifier: 8.5.0
version: 8.5.0 version: 8.5.0
@ -24646,9 +24646,9 @@ packages:
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
'@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231113.3(react-refresh@0.12.0)(webpack@5.86.0)': '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231120.2(react-refresh@0.12.0)(webpack@5.86.0)':
resolution: {registry: https://registry.npmjs.org/, tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231113.3} resolution: {tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231120.2}
id: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231113.3' id: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231120.2'
name: '@vercel/turbopack-ecmascript-runtime' name: '@vercel/turbopack-ecmascript-runtime'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:

View file

@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}

View file

@ -0,0 +1,5 @@
import styles from './style.module.css'
export default function Page() {
return <p className={styles.blue}>hello world</p>
}

View file

@ -0,0 +1,3 @@
.blue {
color: blue;
}

View file

@ -0,0 +1,16 @@
import { nextTestSetup } from 'e2e-utils'
import { describeVariants as describe } from 'next-test-utils'
describe.each(['turbo'])('experimental-lightningcss', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should support css modules', async () => {
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
const $ = await next.render$('/')
expect($('p').text()).toBe('hello world')
// swc_css does not include `-module` in the class name, while lightningcss does.
expect($('p').attr('class')).toBe('style-module__hlQ3RG__blue')
})
})

View file

@ -0,0 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
useLightningcss: true,
},
}
module.exports = nextConfig

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}