Skip to content

Commit

Permalink
use structured images with metainfo (blur placeholder) (#48531)
Browse files Browse the repository at this point in the history
### What?

add support for blur placeholder generation to turbopack

add `StructuredImageModuleType` which is used with `ModuleType::Custom`
to allow importing an image as `{ url, width, height, blurDataURL,
blurWidth, blurHeight }`

in contrast to next.js with webpack this will also generate blur
placeholder in development instead of using a _next/image reference.
This should lead to more production-like experience (at the cost of a
little bit of compilation time).

turbo PR: vercel/turborepo#4621

### Why?

Turbopack was crashing on `placeholder="blur"` before.

fixes WEB-534

### Turbopack changes

* vercel/turborepo#4521 <!-- OJ Kwon -
feat(contextcondition): support InPath contextcondition -->
* vercel/turborepo#4601 <!-- Alex Kirszenberg -
Chunking Context Refactor pt. 3: Address PR comments from pt. 2 -->
* vercel/turborepo#4623 <!-- Tobias Koppers -
exclude turborepo from turbopack bench tests -->
* vercel/turborepo#4399 <!-- Leah - support
require.context -->
* vercel/turborepo#4610 <!-- OJ Kwon - test(subset):
add mdx test into subset -->
* vercel/turborepo#4624 <!-- Tobias Koppers - run
benchmarks on windows and macOS too -->
* vercel/turborepo#4620 <!-- Alex Kirszenberg - Make
ContainmentTree fully generic -->
* vercel/turborepo#4600 <!-- Tobias Koppers - add
getChunkPath method -->
* vercel/turborepo#4621 <!-- Tobias Koppers - add
turbopack-image -->
* vercel/turborepo#4639 <!-- Tobias Koppers -
restrict snapshot path for windows path length limit -->
* vercel/turborepo#4641 <!-- Tobias Koppers - put
webp behind a feature flag -->
  • Loading branch information
sokra authored Apr 20, 2023
1 parent 925bb3b commit 189e6a3
Show file tree
Hide file tree
Showing 29 changed files with 498 additions and 70 deletions.
203 changes: 169 additions & 34 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ swc_relay = { version = "0.2.5" }
testing = { version = "0.33.4" }

# Turbo crates
turbo-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230418.1" }
turbo-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230419.4" }
# [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-230418.1" }
turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230419.4" }
# [TODO]: need to refactor embed_directory! macro usage in next-core
turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230418.1" }
turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230419.4" }

# General Deps

Expand Down
2 changes: 2 additions & 0 deletions packages/next-swc/crates/next-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ turbo-binding = { workspace = true, features = [
"__turbopack_dev_server",
"__turbopack_ecmascript",
"__turbopack_env",
"__turbopack_static",
"__turbopack_image",
"__turbopack_node",
] }
turbo-tasks = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions packages/next-swc/crates/next-core/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"check": "tsc --noEmit"
},
"dependencies": {
"@vercel/turbopack-dev": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-dev/js?turbopack-230418.1",
"@vercel/turbopack-node": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230418.1",
"@vercel/turbopack-dev": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-dev/js?turbopack-230419.4",
"@vercel/turbopack-node": "https://gitpkg.vercel.app/vercel/turbo/crates/turbopack-node/js?turbopack-230419.4",
"anser": "^2.1.1",
"css.escape": "^1.5.1",
"next": "*",
Expand Down
62 changes: 62 additions & 0 deletions packages/next-swc/crates/next-core/src/image/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use anyhow::Result;
use indexmap::indexmap;
use turbo_binding::{
turbo::tasks::Value,
turbopack::{
core::{
asset::AssetVc,
context::{AssetContext, AssetContextVc},
plugin::{CustomModuleType, CustomModuleTypeVc},
resolve::ModulePartVc,
},
ecmascript::{
EcmascriptInputTransformsVc, EcmascriptModuleAssetType, EcmascriptModuleAssetVc,
EcmascriptOptions, InnerAssetsVc,
},
r#static::StaticModuleAssetVc,
},
};

use self::source::StructuredImageSourceAsset;

pub(crate) mod source;

/// Module type that analyzes images and offers some meta information like
/// width, height and blur placeholder as export from the module.
#[turbo_tasks::value]
pub struct StructuredImageModuleType {}

#[turbo_tasks::value_impl]
impl StructuredImageModuleTypeVc {
#[turbo_tasks::function]
pub fn new() -> Self {
StructuredImageModuleTypeVc::cell(StructuredImageModuleType {})
}
}

#[turbo_tasks::value_impl]
impl CustomModuleType for StructuredImageModuleType {
#[turbo_tasks::function]
async fn create_module(
&self,
source: AssetVc,
context: AssetContextVc,
_part: Option<ModulePartVc>,
) -> Result<AssetVc> {
let static_asset = StaticModuleAssetVc::new(source, context);
Ok(EcmascriptModuleAssetVc::new_with_inner_assets(
StructuredImageSourceAsset { image: source }.cell().into(),
context,
Value::new(EcmascriptModuleAssetType::Ecmascript),
EcmascriptInputTransformsVc::empty(),
Value::new(EcmascriptOptions {
..Default::default()
}),
context.compile_time_info(),
InnerAssetsVc::cell(indexmap!(
"IMAGE".to_string() => static_asset.into()
)),
)
.into())
}
}
81 changes: 81 additions & 0 deletions packages/next-swc/crates/next-core/src/image/source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::io::Write;

use anyhow::{bail, Result};
use serde::Serialize;
use turbo_binding::{
turbo::{
tasks::primitives::StringVc,
tasks_fs::{rope::RopeBuilder, FileContent},
},
turbopack::{
core::{
asset::{Asset, AssetContent, AssetContentVc, AssetVc},
ident::AssetIdentVc,
},
ecmascript::utils::StringifyJs,
image::process::{get_meta_data, BlurPlaceholderOptions, BlurPlaceholderOptionsVc},
},
};

fn modifier() -> StringVc {
StringVc::cell("structured image object".to_string())
}

#[turbo_tasks::function]
fn blur_options() -> BlurPlaceholderOptionsVc {
BlurPlaceholderOptions {
quality: 70,
size: 8,
}
.cell()
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageExport<'a> {
width: u32,
height: u32,
#[serde(rename = "blurDataURL")]
blur_data_url: Option<&'a str>,
blur_width: u32,
blur_height: u32,
}

/// An source asset that transforms an image into javascript code which exports
/// an object with meta information like width, height and a blur placeholder.
#[turbo_tasks::value(shared)]
pub struct StructuredImageSourceAsset {
pub image: AssetVc,
}

#[turbo_tasks::value_impl]
impl Asset for StructuredImageSourceAsset {
#[turbo_tasks::function]
fn ident(&self) -> AssetIdentVc {
self.image.ident().with_modifier(modifier())
}

#[turbo_tasks::function]
async fn content(&self) -> Result<AssetContentVc> {
let content = self.image.content().await?;
let AssetContent::File(content) = *content else {
bail!("Input source is not a file and can't be transformed into image information");
};
let mut result = RopeBuilder::from("");
let info = get_meta_data(self.image.ident(), content, Some(blur_options())).await?;
let info = ImageExport {
width: info.width,
height: info.height,
blur_width: info.blur_placeholder.as_ref().map_or(0, |p| p.width),
blur_height: info.blur_placeholder.as_ref().map_or(0, |p| p.height),
blur_data_url: info.blur_placeholder.as_ref().map(|p| p.data_url.as_str()),
};
writeln!(result, "import src from \"IMAGE\";",)?;
writeln!(
result,
"export default {{ src, ...{} }}",
StringifyJs(&info)
)?;
Ok(AssetContent::File(FileContent::Content(result.build().into()).cell()).cell())
}
}
2 changes: 2 additions & 0 deletions packages/next-swc/crates/next-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod babel;
mod embed_js;
pub mod env;
mod fallback;
mod image;
pub mod manifest;
mod next_build;
pub mod next_client;
Expand Down Expand Up @@ -46,5 +47,6 @@ pub fn register() {
turbopack::dev_server::register();
turbopack::node::register();
turbopack::turbopack::register();
turbopack::image::register();
include!(concat!(env!("OUT_DIR"), "/register.rs"));
}
2 changes: 2 additions & 0 deletions packages/next-swc/crates/next-core/src/next_client/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ pub async fn get_client_module_options_context(

let module_options_context = ModuleOptionsContext {
custom_ecmascript_transforms: vec![EcmascriptInputTransform::ServerDirective(
// ServerDirective is not implemented yet and always reports an issue.
// We don't have to pass a valid transition name yet, but the API is prepared.
StringVc::cell("TODO".to_string()),
)],
preset_env_versions: Some(env),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
next_client::context::ClientContextType,
next_config::NextConfigVc,
next_shared::transforms::{
get_next_dynamic_transform_rule, get_next_font_transform_rule,
get_next_dynamic_transform_rule, get_next_font_transform_rule, get_next_image_rule,
get_next_modularize_imports_rule, get_next_pages_transforms_rule,
},
};
Expand Down Expand Up @@ -40,5 +40,7 @@ pub async fn get_next_client_transforms_rules(

rules.push(get_next_dynamic_transform_rule(true, false, false, pages_dir).await?);

rules.push(get_next_image_rule());

Ok(rules)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
next_config::NextConfigVc,
next_server::context::ServerContextType,
next_shared::transforms::{
get_next_dynamic_transform_rule, get_next_font_transform_rule,
get_next_dynamic_transform_rule, get_next_font_transform_rule, get_next_image_rule,
get_next_modularize_imports_rule, get_next_pages_transforms_rule,
},
};
Expand Down Expand Up @@ -41,5 +41,7 @@ pub async fn get_next_server_transforms_rules(

rules.push(get_next_dynamic_transform_rule(true, true, is_server_components, pages_dir).await?);

rules.push(get_next_image_rule());

Ok(rules)
}
24 changes: 23 additions & 1 deletion packages/next-swc/crates/next-core/src/next_shared/transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ use turbo_binding::{
CustomTransformVc, CustomTransformer, EcmascriptInputTransform,
EcmascriptInputTransformsVc, TransformContext,
},
turbopack::module_options::{ModuleRule, ModuleRuleCondition, ModuleRuleEffect},
turbopack::module_options::{
ModuleRule, ModuleRuleCondition, ModuleRuleEffect, ModuleType,
},
},
};
use turbo_tasks::trace::TraceRawVcs;

use crate::image::StructuredImageModuleTypeVc;

/// Returns a rule which applies the Next.js page export stripping transform.
pub async fn get_next_pages_transforms_rule(
pages_dir: FileSystemPathVc,
Expand Down Expand Up @@ -80,6 +84,24 @@ impl CustomTransformer for NextJsStripPageExports {
}
}

/// Returns a rule which applies the Next.js dynamic transform.
pub fn get_next_image_rule() -> ModuleRule {
ModuleRule::new(
ModuleRuleCondition::any(vec![
ModuleRuleCondition::ResourcePathEndsWith(".jpg".to_string()),
ModuleRuleCondition::ResourcePathEndsWith(".jpeg".to_string()),
ModuleRuleCondition::ResourcePathEndsWith(".png".to_string()),
ModuleRuleCondition::ResourcePathEndsWith(".webp".to_string()),
ModuleRuleCondition::ResourcePathEndsWith(".avif".to_string()),
ModuleRuleCondition::ResourcePathEndsWith(".apng".to_string()),
ModuleRuleCondition::ResourcePathEndsWith(".gif".to_string()),
]),
vec![ModuleRuleEffect::ModuleType(ModuleType::Custom(
StructuredImageModuleTypeVc::new().into(),
))],
)
}

/// Returns a rule which applies the Next.js dynamic transform.
pub async fn get_next_dynamic_transform_rule(
is_development: bool,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Image from 'next/image'
import { img } from '../components/img'
import broken from '../public/broken.jpeg'
import { useEffect } from 'react'

export default function Home() {
Expand All @@ -13,26 +14,51 @@ export default function Home() {
id="imported"
alt="test imported image"
src={img}
width="100"
height="100"
placeholder="blur"
/>,
<Image
id="local"
alt="test src image"
src="/triangle-black.png"
width="100"
width="116"
height="100"
/>,
]
}

console.log(img)
function runTests() {
it('it should link to imported image', function () {
it('should return image size', function () {
expect(img).toHaveProperty('width', 116)
expect(img).toHaveProperty('height', 100)
})

it('should not return image size for broken images', function () {
expect(broken).toHaveProperty('width', 0)
expect(broken).toHaveProperty('height', 0)
})

it('should have blur placeholder', function () {
expect(img).toHaveProperty(
'blurDataURL',
expect.stringMatching(/^data:image\/png;base64/)
)
expect(img).toHaveProperty('blurWidth', 8)
expect(img).toHaveProperty('blurHeight', 7)
})

it('should not have blur placeholder for broken images', function () {
expect(broken).toHaveProperty('blurDataURL', null)
expect(broken).toHaveProperty('blurWidth', 0)
expect(broken).toHaveProperty('blurHeight', 0)
})

it('should link to imported image', function () {
const img = document.querySelector('#imported')
expect(img.src).toContain(encodeURIComponent('_next/static/assets'))
})

it('it should link to local src image', function () {
it('should link to local src image', function () {
const img = document.querySelector('#local')
expect(img.src).toContain('triangle-black')
})
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
PlainIssue {
severity: Error,
context: "[project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/image/basic/input/public/broken.jpeg",
category: "image",
title: "Processing image failed",
description: "unable to decode image data\n\nCaused by:\n- The image format could not be determined",
detail: "",
documentation_link: "",
source: None,
sub_issues: [],
processing_path: Some(
[
PlainIssueProcessingPathItem {
context: Some(
"[project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/image/basic/input/pages/index.js",
),
description: "Next.js pages directory",
},
],
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
PlainIssue {
severity: Error,
context: "[project]/packages/next-swc/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/context-weak/input/index.js",
category: "parse",
title: "error TP1007 require.context(\".\", false, /.+/, \"weak\") is not statically analyze-able: require.context() only supports 1-3 arguments (mode is not supported)",
description: "",
detail: "",
documentation_link: "",
source: Some(
PlainIssueSource {
asset: PlainAsset {
ident: "[project]/packages/next-swc/crates/next-dev-tests/tests/integration/webpack/chunks/__skipped__/context-weak/input/index.js",
},
start: SourcePos {
line: 19,
column: 23,
},
end: SourcePos {
line: 19,
column: 23,
},
},
),
sub_issues: [],
processing_path: Some(
[],
),
}
Loading

0 comments on commit 189e6a3

Please sign in to comment.