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/turbo#4621

### Why?

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

fixes WEB-534

### Turbopack changes

* vercel/turbo#4521 <!-- OJ Kwon -
feat(contextcondition): support InPath contextcondition -->
* vercel/turbo#4601 <!-- Alex Kirszenberg -
Chunking Context Refactor pt. 3: Address PR comments from pt. 2 -->
* vercel/turbo#4623 <!-- Tobias Koppers -
exclude turborepo from turbopack bench tests -->
* vercel/turbo#4399 <!-- Leah - support
require.context -->
* vercel/turbo#4610 <!-- OJ Kwon - test(subset):
add mdx test into subset -->
* vercel/turbo#4624 <!-- Tobias Koppers - run
benchmarks on windows and macOS too -->
* vercel/turbo#4620 <!-- Alex Kirszenberg - Make
ContainmentTree fully generic -->
* vercel/turbo#4600 <!-- Tobias Koppers - add
getChunkPath method -->
* vercel/turbo#4621 <!-- Tobias Koppers - add
turbopack-image -->
* vercel/turbo#4639 <!-- Tobias Koppers -
restrict snapshot path for windows path length limit -->
* vercel/turbo#4641 <!-- Tobias Koppers - put
webp behind a feature flag -->
  • Loading branch information
sokra committed 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
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
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
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
@@ -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
@@ -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
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
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
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)
}
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
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
@@ -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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -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",
},
],
),
}
@@ -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(
[],
),
}

0 comments on commit 189e6a3

Please sign in to comment.