diff --git a/crates/rspack_loader_swc/src/options.rs b/crates/rspack_loader_swc/src/options.rs index 6ff91f83ecfc..568398ac3b55 100644 --- a/crates/rspack_loader_swc/src/options.rs +++ b/crates/rspack_loader_swc/src/options.rs @@ -2,7 +2,7 @@ use rspack_cacheable::{ cacheable, with::{AsRefStr, AsRefStrConverter}, }; -use rspack_swc_plugin_import::{ImportOptions, RawImportOptions}; +use rspack_swc_plugin_import::{ImportOptions, RawImportOptions, TransformImportOptions}; use serde::Deserialize; use swc_config::{file_pattern::FilePattern, types::BoolConfig}; use swc_core::base::config::{ @@ -14,6 +14,7 @@ use swc_core::base::config::{ #[serde(rename_all = "camelCase", default)] pub struct RawRspackExperiments { pub import: Option>, + pub transform_import: Option>, } #[derive(Default, Deserialize, Debug)] @@ -26,6 +27,7 @@ pub struct RawCollectTypeScriptInfoOptions { #[derive(Default, Debug)] pub(crate) struct RspackExperiments { pub(crate) import: Option>, + pub(crate) transform_import: Option>, } #[derive(Default, Debug)] @@ -47,6 +49,7 @@ impl From for RspackExperiments { import: value .import .map(|i| i.into_iter().map(|v| v.into()).collect()), + transform_import: value.transform_import, } } } diff --git a/crates/rspack_loader_swc/src/transformer.rs b/crates/rspack_loader_swc/src/transformer.rs index e7513f172682..8db954a784c6 100644 --- a/crates/rspack_loader_swc/src/transformer.rs +++ b/crates/rspack_loader_swc/src/transformer.rs @@ -23,7 +23,14 @@ macro_rules! either { #[allow(clippy::too_many_arguments)] pub(crate) fn transform(rspack_experiments: &RspackExperiments) -> impl Pass + '_ { - either!(rspack_experiments.import, |options| { - rspack_swc_plugin_import::plugin_import(options) - }) + ( + // Legacy import API (deprecated) + either!(rspack_experiments.import, |options| { + rspack_swc_plugin_import::plugin_import(options) + }), + // Modern transformImport API + either!(rspack_experiments.transform_import, |options| { + rspack_swc_plugin_import::transform_import(options) + }), + ) } diff --git a/crates/swc_plugin_import/src/lib.rs b/crates/swc_plugin_import/src/lib.rs index a1d1b8dc9f44..f7dfa29d650d 100644 --- a/crates/swc_plugin_import/src/lib.rs +++ b/crates/swc_plugin_import/src/lib.rs @@ -148,9 +148,24 @@ pub struct ImportOptions { pub ignore_style_component: Option>, } +/// Modern configuration for transformImport API +#[derive(Debug, Deserialize, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TransformImportOptions { + /// Library name to match (e.g., "antd") + pub source: String, + /// Use named import instead of default import (default: true) + pub named_import: Option, + /// Output paths as template strings (e.g., `["antd/es/{{ kebabCase filename }}.js"]`) + pub output: Vec, + /// Members to exclude from transformation + pub exclude: Option>, +} + const CUSTOM_JS: &str = "CUSTOM_JS_NAME"; const CUSTOM_STYLE: &str = "CUSTOM_STYLE"; const CUSTOM_STYLE_NAME: &str = "CUSTOM_STYLE_NAME"; +const TRANSFORM_IMPORT_OUTPUT: &str = "TRANSFORM_IMPORT_OUTPUT_"; /// Panic: /// @@ -232,6 +247,56 @@ pub fn plugin_import( visit_mut_pass(ImportPlugin { config, renderer }) } +/// Creates a transform pass for the modern transformImport API +/// +/// This is the modern replacement for plugin_import with a cleaner API design. +pub fn transform_import( + config: &Vec, +) -> swc_core::ecma::visit::VisitMutPass> { + let mut renderer = TemplateEngine::new(); + + // Register helpers (same as plugin_import) + renderer.register_helper("kebabCase", |value| value.to_kebab_case()); + renderer.register_helper("legacyKebabCase", |value| { + identifier_to_legacy_kebab_case(value) + }); + renderer.register_helper("camelCase", |value| value.to_lower_camel_case()); + renderer.register_helper("snakeCase", |value| value.to_snake_case()); + renderer.register_helper("legacySnakeCase", |value| { + identifier_to_legacy_snake_case(value) + }); + renderer.register_helper("upperCase", |value| { + value.cow_to_ascii_uppercase().into_owned() + }); + renderer.register_helper("lowerCase", |value| { + value.cow_to_ascii_lowercase().into_owned() + }); + + // Register output templates for each config + config.iter().enumerate().for_each(|(index, item)| { + for (output_idx, output_tpl) in item.output.iter().enumerate() { + match Template::parse(output_tpl) { + Ok(template) => { + let template_name = format!("{}{}{}", item.source, TRANSFORM_IMPORT_OUTPUT, output_idx); + renderer.register_template(template_name, template); + } + Err(e) => { + HANDLER.with(|handler| { + handler.err(&format!( + "[builtin:swc-loader] Failed to parse option \"rspackExperiments.transformImport[{}].output[{}]\".\nReason: {}", + index, + output_idx, + &e.to_string() + )) + }); + } + } + } + }); + + visit_mut_pass(TransformImportPlugin { config, renderer }) +} + #[derive(Debug)] struct EsSpec { source: String, @@ -547,3 +612,216 @@ fn render_context(s: String) -> HashMap<&'static str, String> { ctx.insert("member", s); ctx } + +fn render_context_filename(s: String) -> HashMap<&'static str, String> { + let mut ctx = HashMap::default(); + ctx.insert("filename", s); + ctx +} + +pub struct TransformImportPlugin<'a> { + pub config: &'a Vec, + pub renderer: TemplateEngine<'a>, +} + +impl TransformImportPlugin<'_> { + /// Transform a named import member according to the config. + /// Returns a vector of output paths (the first is the JS import, the rest are side-effect imports like CSS). + fn transform(&self, name: &str, config: &TransformImportOptions) -> Vec { + // Check if excluded + if config + .exclude + .as_ref() + .is_some_and(|list| list.iter().any(|c| c == name)) + { + return vec![]; + } + + let ctx = render_context_filename(name.to_string()); + let mut outputs = vec![]; + + for output_idx in 0..config.output.len() { + let template_name = format!("{}{}{}", config.source, TRANSFORM_IMPORT_OUTPUT, output_idx); + match self.renderer.render(&template_name, &ctx) { + Ok(rendered) => outputs.push(rendered), + Err(err) => { + HANDLER.with(|handler| { + handler.err(&format!( + "[builtin:swc-loader] Failed to render \"rspackExperiments.transformImport[i].output[{output_idx}]\".\nReason: {err}", + )) + }); + } + } + } + + outputs + } +} + +impl VisitMut for TransformImportPlugin<'_> { + fn visit_mut_module(&mut self, module: &mut Module) { + // Use visitor to collect all ident references + let mut visitor = IdentComponent { + ident_set: HashSet::default(), + type_ident_set: HashSet::default(), + in_ts_type_ref: false, + }; + module.body.visit_with(&mut visitor); + + let ident_referenced = |ident: &Ident| -> bool { visitor.ident_set.contains(&ident.to_id()) }; + let type_ident_referenced = + |ident: &Ident| -> bool { visitor.type_ident_set.contains(&ident.to_id()) }; + + // Collect new specifiers to add + let mut new_js_imports: Vec = vec![]; + let mut new_side_effect_imports: Vec = vec![]; + let mut specifiers_rm_es: HashSet = HashSet::default(); + + let config = &self.config; + + for (item_index, item) in module.body.iter_mut().enumerate() { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item { + let source = &*var.src.value; + + if let Some(child_config) = config + .iter() + .find(|&c| c.source == source.to_string_lossy()) + { + let mut rm_specifier: HashSet = HashSet::default(); + + for (specifier_idx, specifier) in var.specifiers.iter().enumerate() { + if let ImportSpecifier::Named(s) = specifier { + let imported = s.imported.as_ref().map(|imported| match imported { + ModuleExportName::Ident(ident) => ident.sym.to_string(), + ModuleExportName::Str(str) => str.value.to_string_lossy().to_string(), + }); + + let as_name: Option = imported.is_some().then(|| s.local.sym.to_string()); + let ident: String = imported.unwrap_or_else(|| s.local.sym.to_string()); + let mark = s.local.ctxt.as_u32(); + + if ident_referenced(&s.local) { + let outputs = self.transform(&ident, child_config); + + if !outputs.is_empty() { + // First output is the JS import + let use_named_import = child_config.named_import.unwrap_or(true); + new_js_imports.push(EsSpec { + source: outputs[0].clone(), + default_spec: ident, + as_name, + use_default_import: !use_named_import, + mark, + }); + + // Additional outputs are side-effect imports (e.g., CSS) + for output in outputs.into_iter().skip(1) { + new_side_effect_imports.push(output); + } + + rm_specifier.insert(specifier_idx); + } + } else if type_ident_referenced(&s.local) { + // Type referenced - keep it + } else { + // Not referenced, should be tree-shaken + rm_specifier.insert(specifier_idx); + } + } + } + + if rm_specifier.len() == var.specifiers.len() { + // All specifiers removed, remove whole statement + specifiers_rm_es.insert(item_index); + } else { + // Only remove some specifiers + var.specifiers = var + .specifiers + .take() + .into_iter() + .enumerate() + .filter_map(|(idx, spec)| (!rm_specifier.contains(&idx)).then_some(spec)) + .collect(); + } + } + } + } + + // Remove statements with all specifiers removed + module.body = module + .body + .take() + .into_iter() + .enumerate() + .filter_map(|(idx, stmt)| (!specifiers_rm_es.contains(&idx)).then_some(stmt)) + .collect(); + + let body = &mut module.body; + + // Add new JS imports + for js_source in new_js_imports { + let js_source_ref = js_source.source.as_str(); + let dec = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: if js_source.use_default_import { + vec![ImportSpecifier::Default(ImportDefaultSpecifier { + span: DUMMY_SP, + local: Ident { + ctxt: SyntaxContext::from_u32(js_source.mark), + span: Span::new(BytePos::DUMMY, BytePos::DUMMY), + sym: Atom::from(js_source.as_name.unwrap_or(js_source.default_spec).as_str()), + optional: false, + }, + })] + } else { + vec![ImportSpecifier::Named(ImportNamedSpecifier { + span: DUMMY_SP, + imported: if js_source.as_name.is_some() { + Some(ModuleExportName::Ident(Ident { + span: DUMMY_SP, + ctxt: Default::default(), + sym: Atom::from(js_source.default_spec.as_str()), + optional: false, + })) + } else { + None + }, + local: Ident { + ctxt: SyntaxContext::from_u32(js_source.mark), + span: Span::new(BytePos::DUMMY, BytePos::DUMMY), + sym: Atom::from(js_source.as_name.unwrap_or(js_source.default_spec).as_str()), + optional: false, + }, + is_type_only: false, + })] + }, + src: Box::new(Str { + span: DUMMY_SP, + value: Wtf8Atom::from(js_source_ref), + raw: None, + }), + type_only: false, + with: Default::default(), + phase: ImportPhase::default(), + })); + body.insert(0, dec); + } + + // Add side-effect imports (CSS, etc.) + for side_effect_source in new_side_effect_imports { + let dec = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![], + src: Box::new(Str { + span: DUMMY_SP, + value: Wtf8Atom::from(side_effect_source), + raw: None, + }), + type_only: false, + with: Default::default(), + phase: ImportPhase::default(), + })); + body.insert(0, dec); + } + } +} diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index ea0472d6bac9..45ff8f6784d9 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -521,13 +521,6 @@ type BufferEncodingOption = 'buffer' | { encoding: 'buffer'; }; -// @public -export type BundlerInfoOptions = { - version?: string; - bundler?: string; - force?: boolean | ('version' | 'uniqueId')[]; -}; - // @public (undocumented) type ByPass = (req: Request_2, res: Response_2, proxyConfig: ProxyConfigArrayItem) => any; @@ -1619,6 +1612,7 @@ export type CssAutoGeneratorOptions = { export type CssAutoParserOptions = { namedExports?: CssParserNamedExports; url?: CssParserUrl; + resolveImport?: CssParserResolveImport; }; // @public @@ -1725,6 +1719,7 @@ export type CssModuleGeneratorOptions = CssAutoGeneratorOptions; export type CssModuleParserOptions = { namedExports?: CssParserNamedExports; url?: CssParserUrl; + resolveImport?: CssParserResolveImport; }; // @public (undocumented) @@ -1734,8 +1729,12 @@ export type CssParserNamedExports = boolean; export type CssParserOptions = { namedExports?: CssParserNamedExports; url?: CssParserUrl; + resolveImport?: CssParserResolveImport; }; +// @public (undocumented) +export type CssParserResolveImport = boolean | ((url: string, media: string | undefined, resourcePath: string, supports: string | undefined, layer: string | undefined) => boolean); + // @public (undocumented) export type CssParserUrl = boolean; @@ -2450,7 +2449,9 @@ export type Experiments = { layers?: boolean; incremental?: IncrementalPresets | Incremental; futureDefaults?: boolean; + rspackFuture?: RspackFutureOptions; buildHttp?: HttpUriOptions; + parallelLoader?: boolean; useInputFileSystem?: UseInputFileSystem; nativeWatcher?: boolean; inlineConst?: boolean; @@ -2493,6 +2494,8 @@ interface Experiments_2 { RslibPlugin: typeof RslibPlugin; // (undocumented) RstestPlugin: typeof RstestPlugin; + // @deprecated (undocumented) + SubresourceIntegrityPlugin: typeof SubresourceIntegrityPlugin; // (undocumented) swc: { transform: typeof transform; @@ -2535,6 +2538,10 @@ export interface ExperimentsNormalized { // (undocumented) outputModule?: boolean; // (undocumented) + parallelLoader?: boolean; + // (undocumented) + rspackFuture?: RspackFutureOptions; + // (undocumented) topLevelAwait?: boolean; // (undocumented) typeReexportsPresence?: boolean; @@ -3124,6 +3131,7 @@ export type HtmlRspackPluginOptions = { chunks?: string[]; excludeChunks?: string[]; chunksSortMode?: 'auto' | 'manual'; + sri?: 'sha256' | 'sha384' | 'sha512'; minify?: boolean; favicon?: string; meta?: Record>; @@ -4201,6 +4209,7 @@ type KnownStatsModule = { failed?: boolean; errors?: number; warnings?: number; + profile?: StatsProfile; reasons?: StatsModuleReason[]; usedExports?: boolean | string[] | null; providedExports?: string[] | null; @@ -4264,6 +4273,13 @@ type KnownStatsPrinterContext = { chunkGroupKind?: string; }; +// @public (undocumented) +type KnownStatsProfile = { + total: number; + resolving: number; + building: number; +}; + // @public (undocumented) interface LabeledStatement extends Node_4, HasSpan { // (undocumented) @@ -5608,9 +5624,9 @@ export type Output = { devtoolModuleFilenameTemplate?: DevtoolModuleFilenameTemplate; devtoolFallbackModuleFilenameTemplate?: DevtoolFallbackModuleFilenameTemplate; chunkLoadTimeout?: number; + charset?: boolean; environment?: Environment; compareBeforeEmit?: boolean; - bundlerInfo?: BundlerInfoOptions; }; // @public (undocumented) @@ -5659,7 +5675,7 @@ export interface OutputNormalized { // (undocumented) asyncChunks?: boolean; // (undocumented) - bundlerInfo?: BundlerInfoOptions; + charset?: boolean; // (undocumented) chunkFilename?: ChunkFilename; // (undocumented) @@ -5911,6 +5927,9 @@ interface PrivateProperty extends ClassPropertyBase { type: "PrivateProperty"; } +// @public +export type Profile = boolean; + // @public (undocumented) type Program = Module_2 | Script; @@ -6822,6 +6841,7 @@ declare namespace rspackExports { AssetParserOptions, CssParserNamedExports, CssParserUrl, + CssParserResolveImport, CssParserOptions, CssAutoParserOptions, CssModuleParserOptions, @@ -6891,7 +6911,7 @@ declare namespace rspackExports { OptimizationSplitChunksOptions, Optimization, ExperimentCacheOptions, - BundlerInfoOptions, + RspackFutureOptions, LazyCompilationOptions, Incremental, IncrementalPresets, @@ -6903,6 +6923,7 @@ declare namespace rspackExports { DevServer, DevServerMiddleware, IgnoreWarnings, + Profile, Amd, Bail, Performance_2 as Performance, @@ -6911,6 +6932,15 @@ declare namespace rspackExports { } } +// @public +export type RspackFutureOptions = { + bundlerInfo?: { + version?: string; + bundler?: string; + force?: boolean | ('version' | 'uniqueId')[]; + }; +}; + // @public (undocumented) export type RspackOptions = { name?: Name; @@ -6941,6 +6971,7 @@ export type RspackOptions = { plugins?: Plugins; devServer?: DevServer; module?: ModuleOptions; + profile?: Profile; amd?: Amd; bail?: Bail; performance?: Performance_2; @@ -7006,6 +7037,8 @@ export interface RspackOptionsNormalized { // (undocumented) plugins: Plugins; // (undocumented) + profile?: Profile; + // (undocumented) resolve: Resolve; // (undocumented) resolveLoader: Resolve; @@ -7724,6 +7757,9 @@ class StatsPrinter { // @public (undocumented) type StatsPrinterContext = KnownStatsPrinterContext & Record; +// @public (undocumented) +type StatsProfile = KnownStatsProfile & Record; + // @public export type StatsValue = boolean | StatsOptions | StatsPresets; diff --git a/packages/rspack/src/builtin-loader/swc/index.ts b/packages/rspack/src/builtin-loader/swc/index.ts index aa0d5c633687..533abd0f839d 100644 --- a/packages/rspack/src/builtin-loader/swc/index.ts +++ b/packages/rspack/src/builtin-loader/swc/index.ts @@ -2,6 +2,8 @@ export type { CollectTypeScriptInfoOptions } from './collectTypeScriptInfo'; export { resolveCollectTypeScriptInfo } from './collectTypeScriptInfo'; export type { PluginImportOptions } from './pluginImport'; export { resolvePluginImport } from './pluginImport'; +export type { TransformImportOptions } from './transformImport'; +export { resolveTransformImport } from './transformImport'; export type { SwcLoaderEnvConfig, diff --git a/packages/rspack/src/builtin-loader/swc/transformImport.ts b/packages/rspack/src/builtin-loader/swc/transformImport.ts new file mode 100644 index 000000000000..b6e66d825544 --- /dev/null +++ b/packages/rspack/src/builtin-loader/swc/transformImport.ts @@ -0,0 +1,32 @@ +export type TransformImportConfig = { + source: string; + namedImport?: boolean; + output: string[]; + exclude?: string[]; +}; + +export type TransformImportOptions = TransformImportConfig[]; + +type RawTransformImportConfig = { + source: string; + namedImport?: boolean; + output: string[]; + exclude?: string[]; +}; + +function resolveTransformImport( + transformImport: TransformImportOptions, +): RawTransformImportConfig[] | undefined { + if (!transformImport) { + return undefined; + } + + return transformImport.map((config) => ({ + source: config.source, + namedImport: config.namedImport ?? true, + output: config.output, + exclude: config.exclude, + })); +} + +export { resolveTransformImport }; diff --git a/packages/rspack/src/builtin-loader/swc/types.ts b/packages/rspack/src/builtin-loader/swc/types.ts index bf5787cfbee0..707c9d2b5960 100644 --- a/packages/rspack/src/builtin-loader/swc/types.ts +++ b/packages/rspack/src/builtin-loader/swc/types.ts @@ -11,6 +11,7 @@ import type { } from '@swc/types'; import type { CollectTypeScriptInfoOptions } from './collectTypeScriptInfo'; import type { PluginImportOptions } from './pluginImport'; +import type { TransformImportOptions } from './transformImport'; export type SwcLoaderEnvConfig = EnvConfig; export type SwcLoaderJscConfig = JscConfig; export type SwcLoaderModuleConfig = ModuleConfig; @@ -30,6 +31,14 @@ export type SwcLoaderOptions = Config & { * @experimental */ rspackExperiments?: { + /** + * Transform named imports from a library to individual module imports. + * This is a modern replacement for the deprecated `import` option. + */ + transformImport?: TransformImportOptions; + /** + * @deprecated Use `transformImport` instead. This option will be removed in Rspack v2.0. + */ import?: PluginImportOptions; /** * @deprecated Use top-level `collectTypeScriptInfo` instead. diff --git a/packages/rspack/src/config/adapterRuleUse.ts b/packages/rspack/src/config/adapterRuleUse.ts index 87bb5415082f..44311b158dbb 100644 --- a/packages/rspack/src/config/adapterRuleUse.ts +++ b/packages/rspack/src/config/adapterRuleUse.ts @@ -2,6 +2,7 @@ import type { AssetInfo, RawModuleRuleUse, RawOptions } from '@rspack/binding'; import { resolveCollectTypeScriptInfo, resolvePluginImport, + resolveTransformImport, } from '../builtin-loader'; import { type FeatureOptions, @@ -506,10 +507,21 @@ const getSwcLoaderOptions: GetLoaderOptions = (options, _) => { ); } - // resolve `rspackExperiments.import` options + // resolve `rspackExperiments` options const { rspackExperiments } = options; if (rspackExperiments) { + // resolve `rspackExperiments.transformImport` options (new API) + if (rspackExperiments.transformImport) { + rspackExperiments.transformImport = resolveTransformImport( + rspackExperiments.transformImport, + ); + } + + // resolve `rspackExperiments.import` options (deprecated API) if (rspackExperiments.import || rspackExperiments.pluginImport) { + deprecate( + '`rspackExperiments.import` is deprecated and will be removed in Rspack v2.0. Use `rspackExperiments.transformImport` instead.', + ); rspackExperiments.import = resolvePluginImport( rspackExperiments.import || rspackExperiments.pluginImport, ); diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/basic.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/basic.js new file mode 100644 index 000000000000..fef69c6b0ccb --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/basic.js @@ -0,0 +1,5 @@ +import { FooBar } from "./src/basic"; + +it("basic transformImport", () => { + expect(FooBar).toBe("FooBar"); +}); diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/default-import.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/default-import.js new file mode 100644 index 000000000000..254410bb0e1f --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/default-import.js @@ -0,0 +1,5 @@ +import { FooBar } from "./src/default-import"; + +it("transformImport with namedImport: false (default import)", () => { + expect(FooBar).toBe("FooBar"); +}); diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/exclude.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/exclude.js new file mode 100644 index 000000000000..c2a2ba2c70e5 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/exclude.js @@ -0,0 +1,7 @@ +import { Included, Excluded } from "./src/exclude"; + +it("transformImport with exclude", () => { + expect(Included).toBe("Included"); + // Excluded should still work since it's not transformed but stays as named export from source + expect(Excluded).toBe("Excluded"); +}); diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/helpers.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/helpers.js new file mode 100644 index 000000000000..2a5a94128144 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/helpers.js @@ -0,0 +1,5 @@ +import { FooBar } from "./src/helpers"; + +it("transformImport with various helpers (camelCase, snakeCase, upperCase, lowerCase)", () => { + expect(FooBar).toBe("FooBar"); +}); diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/index.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/index.js new file mode 100644 index 000000000000..eee05e719ba7 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/index.js @@ -0,0 +1,6 @@ +import "./basic"; +import "./with-css"; +import "./named-import"; +import "./default-import"; +import "./exclude"; +import "./helpers"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/named-import.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/named-import.js new file mode 100644 index 000000000000..8893c2b8ed9c --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/named-import.js @@ -0,0 +1,5 @@ +import { FooBar } from "./src/named-import"; + +it("transformImport with namedImport: true", () => { + expect(FooBar).toBe("FooBar"); +}); diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/rspack.config.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/rspack.config.js new file mode 100644 index 000000000000..17caf7c20b95 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/rspack.config.js @@ -0,0 +1,54 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + resolve: {}, + module: { + rules: [ + { + test: /\.css$/, + type: "asset" + }, + { + test: /\.js$/, + loader: "builtin:swc-loader", + options: { + rspackExperiments: { + transformImport: [ + { + source: "./src/basic", + output: ["./src/basic/lib/{{ kebabCase filename }}"] + }, + { + source: "./src/with-css", + output: [ + "./src/with-css/es/{{ kebabCase filename }}.js", + "./src/with-css/css/{{ kebabCase filename }}.css" + ] + }, + { + source: "./src/named-import", + namedImport: true, + output: ["./src/named-import/lib/{{ filename }}"] + }, + { + source: "./src/default-import", + namedImport: false, + output: ["./src/default-import/lib/{{ kebabCase filename }}"] + }, + { + source: "./src/exclude", + output: ["./src/exclude/lib/{{ kebabCase filename }}"], + exclude: ["Excluded"] + }, + { + source: "./src/helpers", + output: [ + "./src/helpers/{{ camelCase filename }}/{{ snakeCase filename }}/{{ upperCase filename }}/{{ lowerCase filename }}" + ] + } + ] + } + } + } + ] + } +}; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/basic/lib/foo-bar/index.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/basic/lib/foo-bar/index.js new file mode 100644 index 000000000000..3711356b5753 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/basic/lib/foo-bar/index.js @@ -0,0 +1 @@ +export default "FooBar"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/default-import/lib/foo-bar/index.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/default-import/lib/foo-bar/index.js new file mode 100644 index 000000000000..3711356b5753 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/default-import/lib/foo-bar/index.js @@ -0,0 +1 @@ +export default "FooBar"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/exclude/index.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/exclude/index.js new file mode 100644 index 000000000000..163a0bb6d6c9 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/exclude/index.js @@ -0,0 +1,3 @@ +// This is the main barrel file that exports both Included and Excluded +// Excluded is not transformed due to the exclude config +export const Excluded = "Excluded"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/exclude/lib/included/index.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/exclude/lib/included/index.js new file mode 100644 index 000000000000..d6cf8c5c81bd --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/exclude/lib/included/index.js @@ -0,0 +1 @@ +export default "Included"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/helpers/fooBar/foo_bar/FOOBAR/foobar/index.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/helpers/fooBar/foo_bar/FOOBAR/foobar/index.js new file mode 100644 index 000000000000..3711356b5753 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/helpers/fooBar/foo_bar/FOOBAR/foobar/index.js @@ -0,0 +1 @@ +export default "FooBar"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/named-import/lib/FooBar/FooBar.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/named-import/lib/FooBar/FooBar.js new file mode 100644 index 000000000000..4839a2fe24f0 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/named-import/lib/FooBar/FooBar.js @@ -0,0 +1 @@ +export const FooBar = "FooBar"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/named-import/lib/FooBar/index.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/named-import/lib/FooBar/index.js new file mode 100644 index 000000000000..5f4d7a33b60e --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/named-import/lib/FooBar/index.js @@ -0,0 +1 @@ +export { FooBar } from "./FooBar"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/with-css/css/foo-bar.css b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/with-css/css/foo-bar.css new file mode 100644 index 000000000000..7c109d32c7ab --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/with-css/css/foo-bar.css @@ -0,0 +1,3 @@ +.foo-bar { + color: red; +} \ No newline at end of file diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/with-css/es/foo-bar.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/with-css/es/foo-bar.js new file mode 100644 index 000000000000..3711356b5753 --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/src/with-css/es/foo-bar.js @@ -0,0 +1 @@ +export default "FooBar"; diff --git a/tests/rspack-test/configCases/builtin-swc-loader/transform-import/with-css.js b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/with-css.js new file mode 100644 index 000000000000..7e963c2bc67c --- /dev/null +++ b/tests/rspack-test/configCases/builtin-swc-loader/transform-import/with-css.js @@ -0,0 +1,5 @@ +import { FooBar } from "./src/with-css"; + +it("transformImport with CSS side-effect", () => { + expect(FooBar).toBe("FooBar"); +}); diff --git a/website/docs/en/guide/features/builtin-swc-loader.mdx b/website/docs/en/guide/features/builtin-swc-loader.mdx index a26a06a6a20d..716d03368187 100644 --- a/website/docs/en/guide/features/builtin-swc-loader.mdx +++ b/website/docs/en/guide/features/builtin-swc-loader.mdx @@ -328,7 +328,11 @@ Experimental features provided by rspack. ### rspackExperiments.import - + + +:::warning Deprecated +`rspackExperiments.import` has been deprecated. Please use [rspackExperiments.transformImport](#rspackexperimentstransformimport) instead, which provides the same functionality with a more advanced API design. +::: Ported from [babel-plugin-import](https://github.com/umijs/babel-plugin-import), configurations are basically the same. @@ -472,6 +476,96 @@ import Button from 'antd/es/button'; import 'antd/es/button/style'; ``` +### rspackExperiments.transformImport + + + +`transformImport` is the successor to [`rspackExperiments.import`](#rspackexperimentsimport), providing the same functionality for transforming module imports with a more advanced API design. This is the recommended approach for new projects, and existing projects are encouraged to migrate from `import` to `transformImport`. + +The core concept is the same as `rspackExperiments.import` - transforming import specifiers from a source module into individual module imports, with optional additional outputs (e.g., styles). + +#### Configuration Options + +| Option | Type | Description | +| ------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `source` | `string` | The source module to match, e.g., `'antd'` or `'./src/components'` | +| `output` | `string[]` | Array of output templates for the transformed imports. Use `{{ filename }}` as placeholder for the import specifier, only the first output contains the imported specifier | +| `namedImport` | `boolean` | Whether to keep the import as a named import. Default: `false` (converts to default import) | +| `exclude` | `string[]` | Array of import specifiers to exclude from transformation | + +#### Template Helpers + +The `output` templates support the following helpers: + +- `{{ filename }}` - The original import specifier +- `{{ kebabCase filename }}` - Convert to kebab-case (e.g., `MyButton` → `my-button`) +- `{{ camelCase filename }}` - Convert to camelCase +- `{{ snakeCase filename }}` - Convert to snake_case +- `{{ upperCase filename }}` - Convert to UPPERCASE +- `{{ lowerCase filename }}` - Convert to lowercase + +#### Example + +```js title="rspack.config.mjs" +export default { + module: { + rules: [ + { + test: /\.js$/, + loader: 'builtin:swc-loader', + options: { + rspackExperiments: { + transformImport: [ + { + source: './src/components', + output: [ + './src/components/lib/{{ kebabCase filename }}', + './src/components/lib/{{ kebabCase filename }}/style.css', + ], + }, + { + source: './src/ui', + output: [ + './src/ui/es/{{ kebabCase filename }}.js', + './src/ui/css/{{ kebabCase filename }}.css', + ], + }, + { + source: './src/utils', + namedImport: true, + output: ['./src/utils/lib/{{ filename }}'], + }, + { + source: './src/exclude', + output: ['./src/exclude/lib/{{ kebabCase filename }}'], + exclude: ['Excluded'], + }, + ], + }, + }, + }, + ], + }, +}; +``` + +With this configuration: + +```ts +import { MyButton, MyInput } from './src/components'; +``` + +Will be transformed to: + +```ts +import MyButton from './src/components/lib/my-button'; +import './src/components/lib/my-button/style.css'; +import MyInput from './src/components/lib/my-input'; +import './src/components/lib/my-input/style.css'; +``` + +For more examples, see the [test configuration](https://github.com/web-infra-dev/rspack/blob/main/tests/rspack-test/configCases/builtin-swc-loader/transform-import/rspack.config.js). + ### collectTypeScriptInfo diff --git a/website/docs/zh/guide/features/builtin-swc-loader.mdx b/website/docs/zh/guide/features/builtin-swc-loader.mdx index fc5aa64e5908..bd581122fd7c 100644 --- a/website/docs/zh/guide/features/builtin-swc-loader.mdx +++ b/website/docs/zh/guide/features/builtin-swc-loader.mdx @@ -328,7 +328,11 @@ Rspack 内置的实验性功能。 ### rspackExperiments.import - + + +:::warning 已废弃 +`rspackExperiments.import` 已被废弃。请使用 [rspackExperiments.transformImport](#rspackexperimentstransformimport) 替代,它提供相同的功能并具有更先进的 API 设计。 +::: 移植自 [babel-plugin-import](https://github.com/umijs/babel-plugin-import),配置基本保持一致。 @@ -475,6 +479,96 @@ import Button from 'antd/es/button'; import 'antd/es/button/style'; ``` +### rspackExperiments.transformImport + + + +`transformImport` 是 [`rspackExperiments.import`](#rspackexperimentsimport) 的后继版本,提供相同的模块导入转换功能,更现代易用的 API 设计。推荐新项目使用此配置,现有项目也建议从 `import` 迁移到 `transformImport`。 + +核心概念与 `rspackExperiments.import` 相同——将源模块的导入转换为更加具体的模块导入,并可选地添加额外的输出(例如样式)。 + +#### 配置选项 + +| 选项 | 类型 | 描述 | +| ------------- | ---------- | ------------------------------------------------------------------------------------------------------ | +| `source` | `string` | 要匹配的源模块,例如 `'antd'` 或 `'./src/components'` | +| `output` | `string[]` | 转换后导入的输出模板数组。使用 `{{ filename }}` 作为导入说明符的占位符,只有第一个输出包含导入的说明符 | +| `namedImport` | `boolean` | 是否保持命名导入。默认:`false`(转换为默认导入) | +| `exclude` | `string[]` | 要排除转换的导入说明符数组 | + +#### 模板辅助函数 + +`output` 模板支持以下辅助函数: + +- `{{ filename }}` - 原始导入说明符 +- `{{ kebabCase filename }}` - 转换为 kebab-case(例如 `MyButton` → `my-button`) +- `{{ camelCase filename }}` - 转换为 camelCase +- `{{ snakeCase filename }}` - 转换为 snake_case +- `{{ upperCase filename }}` - 转换为大写 +- `{{ lowerCase filename }}` - 转换为小写 + +#### 示例 + +```js title="rspack.config.mjs" +export default { + module: { + rules: [ + { + test: /\.js$/, + loader: 'builtin:swc-loader', + options: { + rspackExperiments: { + transformImport: [ + { + source: './src/components', + output: [ + './src/components/lib/{{ kebabCase filename }}', + './src/components/lib/{{ kebabCase filename }}/style.css', + ], + }, + { + source: './src/ui', + output: [ + './src/ui/es/{{ kebabCase filename }}.js', + './src/ui/css/{{ kebabCase filename }}.css', + ], + }, + { + source: './src/utils', + namedImport: true, + output: ['./src/utils/lib/{{ filename }}'], + }, + { + source: './src/exclude', + output: ['./src/exclude/lib/{{ kebabCase filename }}'], + exclude: ['Excluded'], + }, + ], + }, + }, + }, + ], + }, +}; +``` + +使用此配置: + +```ts +import { MyButton, MyInput } from './src/components'; +``` + +将被转换为: + +```ts +import MyButton from './src/components/lib/my-button'; +import './src/components/lib/my-button/style.css'; +import MyInput from './src/components/lib/my-input'; +import './src/components/lib/my-input/style.css'; +``` + +更多示例请参考 [测试配置](https://github.com/web-infra-dev/rspack/blob/main/tests/rspack-test/configCases/builtin-swc-loader/transform-import/rspack.config.js)。 + ### collectTypeScriptInfo