Skip to content

Commit 3651244

Browse files
authored
feat: add assets mode for asset path generate (#1852)
* feat: add assets mode for asset path generate * test: update test failed
1 parent 92ce692 commit 3651244

File tree

19 files changed

+156
-42
lines changed

19 files changed

+156
-42
lines changed

.changeset/heavy-rabbits-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@farmfe/core": patch
3+
---
4+
5+
add assets mode for asset path generate

crates/core/src/config/asset.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
use super::TargetEnv;
4+
5+
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
6+
#[serde(rename_all = "camelCase")]
7+
pub enum AssetFormatMode {
8+
Node,
9+
Browser,
10+
}
11+
12+
impl From<TargetEnv> for AssetFormatMode {
13+
fn from(value: TargetEnv) -> Self {
14+
if value.is_browser() {
15+
AssetFormatMode::Browser
16+
} else {
17+
AssetFormatMode::Node
18+
}
19+
}
20+
}
21+
22+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23+
#[serde(rename_all = "camelCase", default)]
24+
pub struct AssetsConfig {
25+
pub include: Vec<String>,
26+
/// Used internally, this option will be not exposed to user.
27+
pub public_dir: Option<String>,
28+
// TODO: v2
29+
// for ssr mode, should specify asset path format, default from `output.targetEnv`
30+
// pub mode: Option<AssetFormatMode>,
31+
}

crates/core/src/config/custom.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use std::{collections::HashMap, sync::Arc};
22

3+
use serde::{de::DeserializeOwned, Deserialize};
4+
35
use crate::context::CompilationContext;
46

57
use super::{
8+
asset::AssetFormatMode,
69
config_regex::ConfigRegex,
710
css::NameConversion,
811
external::{ExternalConfig, ExternalObject},
@@ -13,6 +16,7 @@ const CUSTOM_CONFIG_RUNTIME_ISOLATE: &str = "runtime.isolate";
1316
pub const CUSTOM_CONFIG_EXTERNAL_RECORD: &str = "external.record";
1417
pub const CUSTOM_CONFIG_RESOLVE_DEDUPE: &str = "resolve.dedupe";
1518
pub const CUSTOM_CONFIG_CSS_MODULES_LOCAL_CONVERSION: &str = "css.modules.locals_conversion";
19+
pub const CUSTOM_CONFIG_ASSETS_MODE: &str = "assets.mode";
1620

1721
pub fn get_config_runtime_isolate(context: &Arc<CompilationContext>) -> bool {
1822
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 {
2832
return ExternalConfig::new();
2933
}
3034

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

3438
let mut external_config = ExternalConfig::new();
3539

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

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

6060
pub fn get_config_css_modules_local_conversion(config: &Config) -> NameConversion {
61-
if let Some(val) = config
61+
get_field_or_default_from_custom(config, CUSTOM_CONFIG_CSS_MODULES_LOCAL_CONVERSION)
62+
}
63+
64+
pub fn get_config_assets_mode(config: &Config) -> Option<AssetFormatMode> {
65+
get_field_or_default_from_custom(config, CUSTOM_CONFIG_ASSETS_MODE)
66+
}
67+
68+
fn get_field_or_default_from_custom<T: Default + DeserializeOwned>(
69+
config: &Config,
70+
field: &str,
71+
) -> T {
72+
config
6273
.custom
63-
.get(CUSTOM_CONFIG_CSS_MODULES_LOCAL_CONVERSION)
64-
{
65-
serde_json::from_str(val).unwrap_or_default()
66-
} else {
67-
Default::default()
68-
}
74+
.get(field)
75+
.map(|val| serde_json::from_str(val).unwrap_or_default())
76+
.unwrap_or_default()
6977
}

crates/core/src/config/mod.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub const FARM_REQUIRE: &str = "farmRequire";
1818
pub const FARM_MODULE: &str = "module";
1919
pub const FARM_MODULE_EXPORT: &str = "exports";
2020

21+
pub mod asset;
2122
pub mod bool_or_obj;
2223
pub mod comments;
2324
pub mod config_regex;
@@ -33,6 +34,8 @@ pub mod preset_env;
3334
pub mod script;
3435
pub mod tree_shaking;
3536

37+
use asset::AssetsConfig;
38+
3639
pub use output::*;
3740

3841
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -294,14 +297,6 @@ impl Default for RuntimeConfig {
294297
}
295298
}
296299

297-
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
298-
#[serde(rename_all = "camelCase", default)]
299-
pub struct AssetsConfig {
300-
pub include: Vec<String>,
301-
/// Used internally, this option will be not exposed to user.
302-
pub public_dir: Option<String>,
303-
}
304-
305300
#[derive(Debug, Clone, Serialize, Deserialize)]
306301
pub enum SourcemapConfig {
307302
/// Generate inline sourcemap instead of a separate file for mutable resources.

crates/plugin_static_assets/src/lib.rs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
use base64::engine::{general_purpose, Engine};
1010
use farmfe_core::{
1111
cache_item,
12-
config::{Config},
12+
config::{asset::AssetFormatMode, custom::get_config_assets_mode, Config},
1313
context::{CompilationContext, EmitFileParams},
1414
deserialize,
1515
module::ModuleType,
@@ -18,6 +18,7 @@ use farmfe_core::{
1818
resource::{Resource, ResourceOrigin, ResourceType},
1919
rkyv::Deserialize,
2020
serialize,
21+
swc_common::sync::OnceCell,
2122
};
2223
use farmfe_toolkit::{
2324
fs::{read_file_raw, read_file_utf8, transform_output_filename},
@@ -42,11 +43,15 @@ fn is_asset_query(query: &Vec<(String, String)>) -> bool {
4243
query_map.contains_key("raw") || query_map.contains_key("inline") || query_map.contains_key("url")
4344
}
4445

45-
pub struct FarmPluginStaticAssets {}
46+
pub struct FarmPluginStaticAssets {
47+
asset_format_mode: OnceCell<AssetFormatMode>,
48+
}
4649

4750
impl FarmPluginStaticAssets {
4851
pub fn new(_: &Config) -> Self {
49-
Self {}
52+
Self {
53+
asset_format_mode: OnceCell::new(),
54+
}
5055
}
5156

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

176-
let content = format!(
177-
"export default \"data:{mime_type_str};base64,{file_base64}\""
178-
);
181+
let content = format!("export default \"data:{mime_type_str};base64,{file_base64}\"");
179182

180183
return Ok(Some(farmfe_core::plugin::PluginTransformHookResult {
181184
content,
@@ -231,12 +234,23 @@ impl Plugin for FarmPluginStaticAssets {
231234
format!("/{resource_name}")
232235
};
233236

234-
let content = if context.config.output.target_env.is_node() {
235-
format!(
236-
"export default new URL(/* {FARM_IGNORE_ACTION_COMMENT} */{assets_path:?}, import.meta.url)"
237-
)
238-
} else {
239-
format!("export default {assets_path:?};")
237+
let mode = self.asset_format_mode.get_or_init(|| {
238+
get_config_assets_mode(&context.config)
239+
.unwrap_or_else(|| (context.config.output.target_env.clone().into()))
240+
});
241+
242+
let content = match mode {
243+
AssetFormatMode::Node => {
244+
format!(
245+
r#"
246+
import {{ fileURLToPath }} from "node:url";
247+
export default fileURLToPath(new URL(/* {FARM_IGNORE_ACTION_COMMENT} */{assets_path:?}, import.meta.url))
248+
"#
249+
)
250+
}
251+
AssetFormatMode::Browser => {
252+
format!("export default {assets_path:?};")
253+
}
240254
};
241255

242256
context.emit_file(EmitFileParams {

examples/react-ssr/farm.config.server.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ export default {
1111
output: {
1212
path: './dist',
1313
targetEnv: 'node',
14-
format: 'cjs'
14+
format: 'cjs',
15+
publicPath: '/'
1516
},
1617
external: [...builtinModules.map((m) => `^${m}$`)],
1718
css: {
1819
prefixer: {
1920
targets: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 11']
2021
}
22+
},
23+
assets: {
24+
mode: 'browser'
2125
}
2226
},
2327
plugins: [

examples/react-ssr/src/logo.png

51.6 KB
Loading

examples/react-ssr/src/main.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React from 'react';
2-
import { Routes, Route, Outlet, Link } from 'react-router-dom';
1+
import React from "react";
2+
import { Routes, Route, Outlet, Link } from "react-router-dom";
3+
import logo from "./logo.png";
34

45
export default function App() {
56
return (
@@ -12,6 +13,8 @@ export default function App() {
1213
server!
1314
</p>
1415

16+
<img style={{ width: 350, height: 100 }} src={logo} alt="logo" />
17+
1518
<p>
1619
This is great for search engines that need to index this page. It's also
1720
great for users because server-rendered pages tend to load more quickly

examples/react-ssr/src/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "*.png" {
2+
declare const v: string;
3+
export default v;
4+
}

examples/tree-shake-antd/e2e.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { test, expect, describe } from 'vitest';
22
import { startProjectAndTest } from '../../e2e/vitestSetup';
33
import { basename, dirname } from 'path';
44
import { fileURLToPath } from 'url';
5+
import { execa } from 'execa';
56

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

27+
await execa('npm', ['run', 'build'], {
28+
cwd: projectPath,
29+
})
30+
2631
test(`exmaples ${name} run start`, async () => {
2732
await runTest();
2833
});

packages/core/src/config/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export const CUSTOM_KEYS = {
1313
external_record: 'external.record',
1414
runtime_isolate: 'runtime.isolate',
1515
resolve_dedupe: 'resolve.dedupe',
16-
css_locals_conversion: 'css.modules.locals_conversion'
16+
css_locals_conversion: 'css.modules.locals_conversion',
17+
assets_mode: 'assets.mode'
1718
};
1819

1920
export const FARM_RUST_PLUGIN_FUNCTION_ENTRY = 'func.js';

packages/core/src/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
FARM_DEFAULT_NAMESPACE
5757
} from './constants.js';
5858
import { mergeConfig, mergeFarmCliConfig } from './mergeConfig.js';
59+
import { normalizeAsset } from './normalize-config/normalize-asset.js';
5960
import { normalizeCss } from './normalize-config/normalize-css.js';
6061
import { normalizeExternal } from './normalize-config/normalize-external.js';
6162
import { normalizeResolve } from './normalize-config/normalize-resolve.js';
@@ -560,6 +561,7 @@ export async function normalizeUserCompilationConfig(
560561

561562
normalizeResolve(userConfig, resolvedCompilation);
562563
normalizeCss(userConfig, resolvedCompilation);
564+
normalizeAsset(userConfig, resolvedCompilation);
563565

564566
return resolvedCompilation;
565567
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { CUSTOM_KEYS } from '../constants.js';
2+
import { ResolvedCompilation, UserConfig } from '../types.js';
3+
4+
export function normalizeAsset(
5+
config: UserConfig,
6+
resolvedCompilation: ResolvedCompilation
7+
) {
8+
if (config.compilation?.assets?.mode) {
9+
const mode = config.compilation.assets.mode;
10+
11+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
12+
resolvedCompilation.custom![CUSTOM_KEYS.assets_mode] = JSON.stringify(mode);
13+
}
14+
}

packages/core/src/config/normalize-config/normalize-output.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ function tryGetDefaultPublicPath(
188188
return publicPath;
189189
}
190190

191+
if (publicPath) {
192+
return publicPath;
193+
}
194+
191195
if (targetEnv === 'node' && isAbsolute(publicPath)) {
192196
// vitejs plugin maybe set absolute path, should transform to relative path
193197
const relativePath = './' + path.posix.normalize(publicPath).slice(1);

packages/core/src/config/schema.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ const compilationConfigSchema = z
102102
.optional(),
103103
assets: z
104104
.object({
105-
include: z.array(z.string()).optional()
105+
include: z.array(z.string()).optional(),
106+
publicDir: z.string().optional(),
107+
mode: z.enum(['browser', 'node']).optional()
106108
})
107109
.strict()
108110
.optional(),

packages/core/src/config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export interface ResolvedCompilation
121121
resolve?: {
122122
dedupe?: never;
123123
} & Config['config']['resolve'];
124+
assets?: Omit<Config['config']['assets'], 'mode'>;
124125
css?: ResolvedCss;
125126
}
126127

packages/core/src/types/binding.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ export interface Config {
434434
assets?: {
435435
include?: string[];
436436
publicDir?: string;
437+
mode?: 'node' | 'browser';
437438
};
438439
script?: ScriptConfig;
439440
css?: CssConfig;

packages/core/tests/config/index.spec.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe('normalizeOutput', () => {
115115
expect(resolvedConfig.output?.publicPath).toEqual('./');
116116
});
117117

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

126126
normalizeOutput(resolvedConfig, true, new NoopLogger());
127127
expect(resolvedConfig.output.targetEnv).toEqual('node');
128-
expect(resolvedConfig.output.publicPath).toEqual('./public/');
128+
expect(resolvedConfig.output.publicPath).toEqual('/public/');
129+
});
130+
131+
test('normalizeOutput with node targetEnv shoud use default publicPath by targetEnv', () => {
132+
(
133+
[
134+
{ targetEnv: 'node', expectPublic: './' },
135+
{ targetEnv: 'browser', expectPublic: '/' }
136+
] as const
137+
).forEach((item) => {
138+
const resolvedConfig: ResolvedCompilation = {
139+
output: {
140+
targetEnv: item.targetEnv
141+
}
142+
};
143+
144+
normalizeOutput(resolvedConfig, true, new NoopLogger());
145+
expect(resolvedConfig.output.targetEnv).toEqual(item.targetEnv);
146+
expect(resolvedConfig.output.publicPath).toEqual(item.expectPublic);
147+
});
129148
});
130149
});

vitest.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineConfig({
1010
environment: 'node',
1111
deps: {
1212
interopDefault: false
13-
}
13+
},
14+
retry: 5
1415
}
1516
});

0 commit comments

Comments
 (0)