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: add assets mode for asset path generate #1852

Merged
merged 3 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changeset/heavy-rabbits-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@farmfe/core": patch
---

add assets mode for asset path generate
31 changes: 31 additions & 0 deletions crates/core/src/config/asset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use serde::{Deserialize, Serialize};

use super::TargetEnv;

#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AssetFormatMode {
Node,
Browser,
}

impl From<TargetEnv> for AssetFormatMode {
fn from(value: TargetEnv) -> Self {
if value.is_browser() {
AssetFormatMode::Browser
} else {
AssetFormatMode::Node
}
}
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct AssetsConfig {
pub include: Vec<String>,
/// Used internally, this option will be not exposed to user.
pub public_dir: Option<String>,
// TODO: v2
// for ssr mode, should specify asset path format, default from `output.targetEnv`
// pub mode: Option<AssetFormatMode>,
}
36 changes: 22 additions & 14 deletions crates/core/src/config/custom.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::{collections::HashMap, sync::Arc};

use serde::{de::DeserializeOwned, Deserialize};

use crate::context::CompilationContext;

use super::{
asset::AssetFormatMode,
config_regex::ConfigRegex,
css::NameConversion,
external::{ExternalConfig, ExternalObject},
Expand All @@ -13,6 +16,7 @@ const CUSTOM_CONFIG_RUNTIME_ISOLATE: &str = "runtime.isolate";
pub const CUSTOM_CONFIG_EXTERNAL_RECORD: &str = "external.record";
pub const CUSTOM_CONFIG_RESOLVE_DEDUPE: &str = "resolve.dedupe";
pub const CUSTOM_CONFIG_CSS_MODULES_LOCAL_CONVERSION: &str = "css.modules.locals_conversion";
pub const CUSTOM_CONFIG_ASSETS_MODE: &str = "assets.mode";

pub fn get_config_runtime_isolate(context: &Arc<CompilationContext>) -> bool {
if let Some(val) = context.config.custom.get(CUSTOM_CONFIG_RUNTIME_ISOLATE) {
Expand All @@ -28,8 +32,8 @@ pub fn get_config_external_record(config: &Config) -> ExternalConfig {
return ExternalConfig::new();
}

let external: HashMap<String, String> = serde_json::from_str(val)
.unwrap_or_else(|_| panic!("failed parse record external {val:?}"));
let external: HashMap<String, String> =
serde_json::from_str(val).unwrap_or_else(|_| panic!("failed parse record external {val:?}"));

let mut external_config = ExternalConfig::new();

Expand All @@ -50,20 +54,24 @@ pub fn get_config_external_record(config: &Config) -> ExternalConfig {
}

pub fn get_config_resolve_dedupe(config: &Config) -> Vec<String> {
if let Some(val) = config.custom.get(CUSTOM_CONFIG_RESOLVE_DEDUPE) {
serde_json::from_str(val).unwrap_or_else(|_| vec![])
} else {
vec![]
}
get_field_or_default_from_custom(config, CUSTOM_CONFIG_RESOLVE_DEDUPE)
}

pub fn get_config_css_modules_local_conversion(config: &Config) -> NameConversion {
if let Some(val) = config
get_field_or_default_from_custom(config, CUSTOM_CONFIG_CSS_MODULES_LOCAL_CONVERSION)
}

pub fn get_config_assets_mode(config: &Config) -> Option<AssetFormatMode> {
get_field_or_default_from_custom(config, CUSTOM_CONFIG_ASSETS_MODE)
}

fn get_field_or_default_from_custom<T: Default + DeserializeOwned>(
config: &Config,
field: &str,
) -> T {
config
.custom
.get(CUSTOM_CONFIG_CSS_MODULES_LOCAL_CONVERSION)
{
serde_json::from_str(val).unwrap_or_default()
} else {
Default::default()
}
.get(field)
.map(|val| serde_json::from_str(val).unwrap_or_default())
.unwrap_or_default()
}
11 changes: 3 additions & 8 deletions crates/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub const FARM_REQUIRE: &str = "farmRequire";
pub const FARM_MODULE: &str = "module";
pub const FARM_MODULE_EXPORT: &str = "exports";

pub mod asset;
pub mod bool_or_obj;
pub mod comments;
pub mod config_regex;
Expand All @@ -33,6 +34,8 @@ pub mod preset_env;
pub mod script;
pub mod tree_shaking;

use asset::AssetsConfig;

pub use output::*;

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -294,14 +297,6 @@ impl Default for RuntimeConfig {
}
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct AssetsConfig {
pub include: Vec<String>,
/// Used internally, this option will be not exposed to user.
pub public_dir: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SourcemapConfig {
/// Generate inline sourcemap instead of a separate file for mutable resources.
Expand Down
38 changes: 26 additions & 12 deletions crates/plugin_static_assets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::{
use base64::engine::{general_purpose, Engine};
use farmfe_core::{
cache_item,
config::{Config},
config::{asset::AssetFormatMode, custom::get_config_assets_mode, Config},
context::{CompilationContext, EmitFileParams},
deserialize,
module::ModuleType,
Expand All @@ -18,6 +18,7 @@ use farmfe_core::{
resource::{Resource, ResourceOrigin, ResourceType},
rkyv::Deserialize,
serialize,
swc_common::sync::OnceCell,
};
use farmfe_toolkit::{
fs::{read_file_raw, read_file_utf8, transform_output_filename},
Expand All @@ -42,11 +43,15 @@ fn is_asset_query(query: &Vec<(String, String)>) -> bool {
query_map.contains_key("raw") || query_map.contains_key("inline") || query_map.contains_key("url")
}

pub struct FarmPluginStaticAssets {}
pub struct FarmPluginStaticAssets {
asset_format_mode: OnceCell<AssetFormatMode>,
}

impl FarmPluginStaticAssets {
pub fn new(_: &Config) -> Self {
Self {}
Self {
asset_format_mode: OnceCell::new(),
}
}

fn is_asset(&self, ext: &str, context: &Arc<CompilationContext>) -> bool {
Expand Down Expand Up @@ -173,9 +178,7 @@ impl Plugin for FarmPluginStaticAssets {
let mime_type = mime_guess::from_ext(ext).first_or_octet_stream();
let mime_type_str = mime_type.to_string();

let content = format!(
"export default \"data:{mime_type_str};base64,{file_base64}\""
);
let content = format!("export default \"data:{mime_type_str};base64,{file_base64}\"");

return Ok(Some(farmfe_core::plugin::PluginTransformHookResult {
content,
Expand Down Expand Up @@ -231,12 +234,23 @@ impl Plugin for FarmPluginStaticAssets {
format!("/{resource_name}")
};

let content = if context.config.output.target_env.is_node() {
format!(
"export default new URL(/* {FARM_IGNORE_ACTION_COMMENT} */{assets_path:?}, import.meta.url)"
)
} else {
format!("export default {assets_path:?};")
let mode = self.asset_format_mode.get_or_init(|| {
get_config_assets_mode(&context.config)
.unwrap_or_else(|| (context.config.output.target_env.clone().into()))
});

let content = match mode {
AssetFormatMode::Node => {
format!(
r#"
import {{ fileURLToPath }} from "node:url";
export default fileURLToPath(new URL(/* {FARM_IGNORE_ACTION_COMMENT} */{assets_path:?}, import.meta.url))
"#
)
}
AssetFormatMode::Browser => {
format!("export default {assets_path:?};")
}
};

context.emit_file(EmitFileParams {
Expand Down
6 changes: 5 additions & 1 deletion examples/react-ssr/farm.config.server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ export default {
output: {
path: './dist',
targetEnv: 'node',
format: 'cjs'
format: 'cjs',
publicPath: '/'
},
external: [...builtinModules.map((m) => `^${m}$`)],
css: {
prefixer: {
targets: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 11']
}
},
assets: {
mode: 'browser'
}
},
plugins: [
Expand Down
Binary file added examples/react-ssr/src/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions examples/react-ssr/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { Routes, Route, Outlet, Link } from 'react-router-dom';
import React from "react";
import { Routes, Route, Outlet, Link } from "react-router-dom";
import logo from "./logo.png";

export default function App() {
return (
Expand All @@ -12,6 +13,8 @@ export default function App() {
server!
</p>

<img style={{ width: 350, height: 100 }} src={logo} alt="logo" />

<p>
This is great for search engines that need to index this page. It's also
great for users because server-rendered pages tend to load more quickly
Expand Down
4 changes: 4 additions & 0 deletions examples/react-ssr/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module "*.png" {
declare const v: string;
export default v;
}
5 changes: 5 additions & 0 deletions examples/tree-shake-antd/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { test, expect, describe } from 'vitest';
import { startProjectAndTest } from '../../e2e/vitestSetup';
import { basename, dirname } from 'path';
import { fileURLToPath } from 'url';
import { execa } from 'execa';

const name = basename(import.meta.url);
const projectPath = dirname(fileURLToPath(import.meta.url));
Expand All @@ -23,6 +24,10 @@ describe(`e2e tests - ${name}`, async () => {
command
);

await execa('npm', ['run', 'build'], {
cwd: projectPath,
})

test(`exmaples ${name} run start`, async () => {
await runTest();
});
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const CUSTOM_KEYS = {
external_record: 'external.record',
runtime_isolate: 'runtime.isolate',
resolve_dedupe: 'resolve.dedupe',
css_locals_conversion: 'css.modules.locals_conversion'
css_locals_conversion: 'css.modules.locals_conversion',
assets_mode: 'assets.mode'
};

export const FARM_RUST_PLUGIN_FUNCTION_ENTRY = 'func.js';
2 changes: 2 additions & 0 deletions packages/core/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
FARM_DEFAULT_NAMESPACE
} from './constants.js';
import { mergeConfig, mergeFarmCliConfig } from './mergeConfig.js';
import { normalizeAsset } from './normalize-config/normalize-asset.js';
import { normalizeCss } from './normalize-config/normalize-css.js';
import { normalizeExternal } from './normalize-config/normalize-external.js';
import { normalizeResolve } from './normalize-config/normalize-resolve.js';
Expand Down Expand Up @@ -560,6 +561,7 @@ export async function normalizeUserCompilationConfig(

normalizeResolve(userConfig, resolvedCompilation);
normalizeCss(userConfig, resolvedCompilation);
normalizeAsset(userConfig, resolvedCompilation);

return resolvedCompilation;
}
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/config/normalize-config/normalize-asset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CUSTOM_KEYS } from '../constants.js';
import { ResolvedCompilation, UserConfig } from '../types.js';

export function normalizeAsset(
config: UserConfig,
resolvedCompilation: ResolvedCompilation
) {
if (config.compilation?.assets?.mode) {
const mode = config.compilation.assets.mode;

// biome-ignore lint/style/noNonNullAssertion: <explanation>
resolvedCompilation.custom![CUSTOM_KEYS.assets_mode] = JSON.stringify(mode);
}
}
4 changes: 4 additions & 0 deletions packages/core/src/config/normalize-config/normalize-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ function tryGetDefaultPublicPath(
return publicPath;
}

if (publicPath) {
return publicPath;
}

if (targetEnv === 'node' && isAbsolute(publicPath)) {
// vitejs plugin maybe set absolute path, should transform to relative path
const relativePath = './' + path.posix.normalize(publicPath).slice(1);
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ const compilationConfigSchema = z
.optional(),
assets: z
.object({
include: z.array(z.string()).optional()
include: z.array(z.string()).optional(),
publicDir: z.string().optional(),
mode: z.enum(['browser', 'node']).optional()
})
.strict()
.optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export interface ResolvedCompilation
resolve?: {
dedupe?: never;
} & Config['config']['resolve'];
assets?: Omit<Config['config']['assets'], 'mode'>;
css?: ResolvedCss;
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ export interface Config {
assets?: {
include?: string[];
publicDir?: string;
mode?: 'node' | 'browser';
};
script?: ScriptConfig;
css?: CssConfig;
Expand Down
23 changes: 21 additions & 2 deletions packages/core/tests/config/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('normalizeOutput', () => {
expect(resolvedConfig.output?.publicPath).toEqual('./');
});

test('normalizeOutput with node targetEnv and absolute publicPath', () => {
test('normalizeOutput with node targetEnv and absolute publicPath shoud use user input publicPath', () => {
const resolvedConfig: ResolvedCompilation = {
output: {
targetEnv: 'node',
Expand All @@ -125,6 +125,25 @@ describe('normalizeOutput', () => {

normalizeOutput(resolvedConfig, true, new NoopLogger());
expect(resolvedConfig.output.targetEnv).toEqual('node');
expect(resolvedConfig.output.publicPath).toEqual('./public/');
expect(resolvedConfig.output.publicPath).toEqual('/public/');
});

test('normalizeOutput with node targetEnv shoud use default publicPath by targetEnv', () => {
(
[
{ targetEnv: 'node', expectPublic: './' },
{ targetEnv: 'browser', expectPublic: '/' }
] as const
).forEach((item) => {
const resolvedConfig: ResolvedCompilation = {
output: {
targetEnv: item.targetEnv
}
};

normalizeOutput(resolvedConfig, true, new NoopLogger());
expect(resolvedConfig.output.targetEnv).toEqual(item.targetEnv);
expect(resolvedConfig.output.publicPath).toEqual(item.expectPublic);
});
});
});
Loading