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!
+
+
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
}
});