diff --git a/.changeset/heavy-rabbits-smile.md b/.changeset/heavy-rabbits-smile.md new file mode 100644 index 000000000..e62138a87 --- /dev/null +++ b/.changeset/heavy-rabbits-smile.md @@ -0,0 +1,5 @@ +--- +"@farmfe/core": patch +--- + +add assets mode for asset path generate diff --git a/crates/core/src/config/asset.rs b/crates/core/src/config/asset.rs new file mode 100644 index 000000000..862a2c0c0 --- /dev/null +++ b/crates/core/src/config/asset.rs @@ -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 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, + /// Used internally, this option will be not exposed to user. + pub public_dir: Option, + // TODO: v2 + // for ssr mode, should specify asset path format, default from `output.targetEnv` + // pub mode: Option, +} diff --git a/crates/core/src/config/custom.rs b/crates/core/src/config/custom.rs index 22ba5e1bb..5c002baf2 100644 --- a/crates/core/src/config/custom.rs +++ b/crates/core/src/config/custom.rs @@ -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}, @@ -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) -> bool { if let Some(val) = context.config.custom.get(CUSTOM_CONFIG_RUNTIME_ISOLATE) { @@ -28,8 +32,8 @@ pub fn get_config_external_record(config: &Config) -> ExternalConfig { return ExternalConfig::new(); } - let external: HashMap = serde_json::from_str(val) - .unwrap_or_else(|_| panic!("failed parse record external {val:?}")); + let external: HashMap = + serde_json::from_str(val).unwrap_or_else(|_| panic!("failed parse record external {val:?}")); let mut external_config = ExternalConfig::new(); @@ -50,20 +54,24 @@ pub fn get_config_external_record(config: &Config) -> ExternalConfig { } pub fn get_config_resolve_dedupe(config: &Config) -> Vec { - 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 { + get_field_or_default_from_custom(config, CUSTOM_CONFIG_ASSETS_MODE) +} + +fn get_field_or_default_from_custom( + 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() } diff --git a/crates/core/src/config/mod.rs b/crates/core/src/config/mod.rs index f6ba19c12..cfaa05cfe 100644 --- a/crates/core/src/config/mod.rs +++ b/crates/core/src/config/mod.rs @@ -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; @@ -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)] @@ -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, - /// Used internally, this option will be not exposed to user. - pub public_dir: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SourcemapConfig { /// Generate inline sourcemap instead of a separate file for mutable resources. diff --git a/crates/plugin_static_assets/src/lib.rs b/crates/plugin_static_assets/src/lib.rs index 5e30935c5..5002758d9 100644 --- a/crates/plugin_static_assets/src/lib.rs +++ b/crates/plugin_static_assets/src/lib.rs @@ -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, @@ -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}, @@ -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, +} impl FarmPluginStaticAssets { pub fn new(_: &Config) -> Self { - Self {} + Self { + asset_format_mode: OnceCell::new(), + } } fn is_asset(&self, ext: &str, context: &Arc) -> bool { @@ -225,12 +230,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 { diff --git a/examples/react-ssr/farm.config.server.mjs b/examples/react-ssr/farm.config.server.mjs index 3e82fb218..0f45dbc31 100644 --- a/examples/react-ssr/farm.config.server.mjs +++ b/examples/react-ssr/farm.config.server.mjs @@ -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: [ diff --git a/examples/react-ssr/src/logo.png b/examples/react-ssr/src/logo.png new file mode 100644 index 000000000..67112a5ca Binary files /dev/null and b/examples/react-ssr/src/logo.png differ diff --git a/examples/react-ssr/src/main.tsx b/examples/react-ssr/src/main.tsx index cca325881..28815693b 100644 --- a/examples/react-ssr/src/main.tsx +++ b/examples/react-ssr/src/main.tsx @@ -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 ( @@ -12,6 +13,8 @@ export default function App() { server!

+ logo +

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 diff --git a/examples/react-ssr/src/types.d.ts b/examples/react-ssr/src/types.d.ts new file mode 100644 index 000000000..b9b2dd613 --- /dev/null +++ b/examples/react-ssr/src/types.d.ts @@ -0,0 +1,4 @@ +declare module "*.png" { + declare const v: string; + export default v; +} \ No newline at end of file diff --git a/examples/tree-shake-antd/e2e.spec.ts b/examples/tree-shake-antd/e2e.spec.ts index 2f5a14882..db6ac6a1a 100644 --- a/examples/tree-shake-antd/e2e.spec.ts +++ b/examples/tree-shake-antd/e2e.spec.ts @@ -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)); @@ -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(); }); diff --git a/packages/core/src/config/constants.ts b/packages/core/src/config/constants.ts index 34ca7d11b..08bbc34f3 100644 --- a/packages/core/src/config/constants.ts +++ b/packages/core/src/config/constants.ts @@ -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'; diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index 7ebe6c15f..55b30a15a 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -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'; @@ -560,6 +561,7 @@ export async function normalizeUserCompilationConfig( normalizeResolve(userConfig, resolvedCompilation); normalizeCss(userConfig, resolvedCompilation); + normalizeAsset(userConfig, resolvedCompilation); return resolvedCompilation; } diff --git a/packages/core/src/config/normalize-config/normalize-asset.ts b/packages/core/src/config/normalize-config/normalize-asset.ts new file mode 100644 index 000000000..c894762d6 --- /dev/null +++ b/packages/core/src/config/normalize-config/normalize-asset.ts @@ -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: + resolvedCompilation.custom![CUSTOM_KEYS.assets_mode] = JSON.stringify(mode); + } +} diff --git a/packages/core/src/config/normalize-config/normalize-output.ts b/packages/core/src/config/normalize-config/normalize-output.ts index fadede763..386d1fb11 100644 --- a/packages/core/src/config/normalize-config/normalize-output.ts +++ b/packages/core/src/config/normalize-config/normalize-output.ts @@ -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); diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index 0dbd4f02f..e843350b5 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -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(), diff --git a/packages/core/src/config/types.ts b/packages/core/src/config/types.ts index 02fbf8189..4f72214f1 100644 --- a/packages/core/src/config/types.ts +++ b/packages/core/src/config/types.ts @@ -121,6 +121,7 @@ export interface ResolvedCompilation resolve?: { dedupe?: never; } & Config['config']['resolve']; + assets?: Omit; css?: ResolvedCss; } diff --git a/packages/core/src/types/binding.ts b/packages/core/src/types/binding.ts index b5d853122..fb2a31851 100644 --- a/packages/core/src/types/binding.ts +++ b/packages/core/src/types/binding.ts @@ -434,6 +434,7 @@ export interface Config { assets?: { include?: string[]; publicDir?: string; + mode?: 'node' | 'browser'; }; script?: ScriptConfig; css?: CssConfig; diff --git a/packages/core/tests/config/index.spec.ts b/packages/core/tests/config/index.spec.ts index f07a42a7f..cd92fbef9 100644 --- a/packages/core/tests/config/index.spec.ts +++ b/packages/core/tests/config/index.spec.ts @@ -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', @@ -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); + }); }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 795774721..3ed29d88e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ environment: 'node', deps: { interopDefault: false - } + }, + retry: 5 } });