diff --git a/.changeset/witty-otters-juggle.md b/.changeset/witty-otters-juggle.md new file mode 100644 index 000000000..71de5df14 --- /dev/null +++ b/.changeset/witty-otters-juggle.md @@ -0,0 +1,7 @@ +--- +'@farmfe/js-plugin-record-viewer': patch +'@farmfe/js-plugin-less': patch +'@farmfe/core': patch +--- + +Normalize js plugin hooks name diff --git a/Cargo.lock b/Cargo.lock index 488e9cf35..ff7bf9201 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1502,6 +1502,7 @@ dependencies = [ "farmfe_swc_transformer_import_glob", "farmfe_testing_helpers", "farmfe_toolkit", + "farmfe_utils 0.1.0", "once_cell", "swc_ecma_loader", "swc_plugin_proxy", diff --git a/configs/farm-js-plugin.base.config.mjs b/configs/farm-js-plugin.base.config.mjs new file mode 100644 index 000000000..8200cea7b --- /dev/null +++ b/configs/farm-js-plugin.base.config.mjs @@ -0,0 +1,46 @@ +import { builtinModules } from 'module'; + +const format = process.env.FARM_FORMAT || 'cjs'; +const ext = format === 'esm' ? 'mjs' : 'cjs'; +console.log('format', format, ext); + +export function createFarmJsPluginBuildConfig(plugins) { + return { + compilation: { + input: { + index: './src/index.ts' + }, + output: { + path: `build/${format}`, + entryFilename: `[entryName].${ext}`, + targetEnv: 'node', + format + }, + external: [ + ...builtinModules.map((m) => `^${m}$`), + ...builtinModules.map((m) => `^node:${m}$`) + ], + partialBundling: { + enforceResources: [ + { + name: 'index.js', + test: ['.+'] + } + ] + }, + minify: false, + sourcemap: false, + presetEnv: false, + persistentCache: { + envs: { + FARM_FORMAT: format + } + } + }, + server: { + hmr: false + }, + plugins, + }; + +} \ No newline at end of file diff --git a/crates/compiler/src/generate/render_resource_pots.rs b/crates/compiler/src/generate/render_resource_pots.rs index 7a431a37a..6bc6d3f03 100644 --- a/crates/compiler/src/generate/render_resource_pots.rs +++ b/crates/compiler/src/generate/render_resource_pots.rs @@ -41,7 +41,6 @@ pub fn render_resource_pots_and_generate_resources( let cached_meta = cached_resource_pot.meta; resource_pot.meta = cached_meta; - resource_pot.add_resource(cached_resource.resource.name.clone()); cached_resource.resource.info = Some(rendered_resource_pot_info); @@ -188,6 +187,7 @@ pub fn render_resource_pot_generate_resources( let mut param = PluginRenderResourcePotHookParam { content: resource_pot.meta.rendered_content.clone(), + source_map_chain: resource_pot.meta.rendered_map_chain.clone(), resource_pot_info: ChunkResourceInfo::new(resource_pot, context), }; diff --git a/crates/compiler/src/update/find_hmr_boundaries.rs b/crates/compiler/src/update/find_hmr_boundaries.rs index 197a9a633..5ffcb5351 100644 --- a/crates/compiler/src/update/find_hmr_boundaries.rs +++ b/crates/compiler/src/update/find_hmr_boundaries.rs @@ -21,9 +21,9 @@ pub fn find_hmr_boundaries( let mut res = vec![]; let all_path_accepted = find_hmr_accepted_recursively(id, &module_graph, &mut stack, &mut visited, &mut res); - + // if any of the path is not accepted, reload the whole page if !all_path_accepted { - continue; + return HashMap::new(); } boundaries.insert( @@ -50,25 +50,46 @@ fn find_hmr_accepted_recursively( res: &mut Vec>, ) -> bool { let module = module_graph.module(id).unwrap(); + // There is a path from the module to the root that does not have HMR accepted if module_graph.entries.contains_key(id) { return false; - } else if module.module_type.is_script() { - let hmr_accepted = module.meta.as_script().hmr_accepted; + } - if hmr_accepted { - res.push(stack.clone()); + // self accepted, non script modules are not self-acceptable for now + if module.module_type.is_script() && module.meta.as_script().hmr_self_accepted { + res.push(stack.clone()); - return true; - } + return true; } + // check if any of the importers accepts the module if !visited.contains(id) { visited.insert(id.clone()); let parents = module_graph.dependents_ids(id); for parent in parents { + // check if the parent accepts the module + let parent_module = module_graph.module(&parent).unwrap(); + + if !parent_module.module_type.is_script() { + return false; + } + // if the importer accepts the module, push + if parent_module + .meta + .as_script() + .hmr_accepted_deps + .contains(id) + { + let mut cloned_stack = stack.clone(); + cloned_stack.push(parent.clone()); + res.push(cloned_stack); + // skip self recursive check if accepts the module + continue; + } + stack.push(parent.clone()); let all_path_accepted = find_hmr_accepted_recursively(&parent, module_graph, stack, visited, res); @@ -85,7 +106,10 @@ fn find_hmr_accepted_recursively( #[cfg(test)] mod tests { - use std::{collections::HashMap, sync::Arc}; + use std::{ + collections::{HashMap, HashSet}, + sync::Arc, + }; use farmfe_core::{ config::{Config, Mode}, @@ -117,7 +141,7 @@ mod tests { let module_a = module_graph.module_mut(&"A".into()).unwrap(); module_a.module_type = ModuleType::Js; module_a.meta = ModuleMetaData::Script(ScriptModuleMetaData { - hmr_accepted: true, + hmr_self_accepted: true, ..Default::default() }); @@ -134,13 +158,13 @@ mod tests { let module_d = module_graph.module_mut(&"D".into()).unwrap(); module_d.module_type = ModuleType::Js; module_d.meta = ModuleMetaData::Script(ScriptModuleMetaData { - hmr_accepted: true, + hmr_self_accepted: true, ..Default::default() }); let module_c = module_graph.module_mut(&"C".into()).unwrap(); module_c.module_type = ModuleType::Js; module_c.meta = ModuleMetaData::Script(ScriptModuleMetaData { - hmr_accepted: true, + hmr_self_accepted: true, ..Default::default() }); @@ -165,7 +189,65 @@ mod tests { let module_d = module_graph.module_mut(&"D".into()).unwrap(); module_d.module_type = ModuleType::Js; module_d.meta = ModuleMetaData::Script(ScriptModuleMetaData { - hmr_accepted: true, + hmr_self_accepted: true, + ..Default::default() + }); + + let context = create_context(module_graph); + let boundaries = find_hmr_boundaries(&vec!["F".into()], &context); + // Be careful, the order of the paths may not be guaranteed. check the order if the test fails. + assert_eq!(boundaries, HashMap::new()); + } + + #[test] + fn find_hmr_boundaries_deps_1() { + let mut module_graph = construct_test_module_graph(); + + let module_d = module_graph.module_mut(&"D".into()).unwrap(); + module_d.module_type = ModuleType::Js; + module_d.meta = ModuleMetaData::Script(ScriptModuleMetaData { + hmr_self_accepted: false, + hmr_accepted_deps: HashSet::from(["F".into()]), + ..Default::default() + }); + let module_c = module_graph.module_mut(&"C".into()).unwrap(); + module_c.module_type = ModuleType::Js; + module_c.meta = ModuleMetaData::Script(ScriptModuleMetaData { + hmr_self_accepted: false, + hmr_accepted_deps: HashSet::from(["F".into()]), + ..Default::default() + }); + + let context = create_context(module_graph); + let boundaries = find_hmr_boundaries(&vec!["F".into()], &context); + // Be careful, the order of the paths may not be guaranteed. check the order if the test fails. + assert_eq!( + boundaries, + vec![( + "F".into(), + vec![vec!["F".into(), "D".into()], vec!["F".into(), "C".into()]] + )] + .into_iter() + .collect::>() + ); + } + + #[test] + fn find_hmr_boundaries_deps_2() { + let mut module_graph = construct_test_module_graph(); + + let module_d = module_graph.module_mut(&"D".into()).unwrap(); + module_d.module_type = ModuleType::Js; + module_d.meta = ModuleMetaData::Script(ScriptModuleMetaData { + hmr_self_accepted: false, + hmr_accepted_deps: HashSet::from(["F".into()]), + ..Default::default() + }); + + let module_f = module_graph.module_mut(&"F".into()).unwrap(); + module_f.module_type = ModuleType::Js; + module_f.meta = ModuleMetaData::Script(ScriptModuleMetaData { + hmr_self_accepted: false, ..Default::default() }); @@ -174,4 +256,41 @@ mod tests { // Be careful, the order of the paths may not be guaranteed. check the order if the test fails. assert_eq!(boundaries, HashMap::new()); } + + #[test] + fn find_hmr_boundaries_deps_3() { + let mut module_graph = construct_test_module_graph(); + + let module_b = module_graph.module_mut(&"B".into()).unwrap(); + module_b.module_type = ModuleType::Js; + module_b.meta = ModuleMetaData::Script(ScriptModuleMetaData { + hmr_self_accepted: false, + hmr_accepted_deps: HashSet::from(["E".into()]), + ..Default::default() + }); + + let module_e = module_graph.module_mut(&"E".into()).unwrap(); + module_e.module_type = ModuleType::Js; + module_e.meta = ModuleMetaData::Script(ScriptModuleMetaData { + hmr_self_accepted: false, + ..Default::default() + }); + + let module_g = module_graph.module_mut(&"G".into()).unwrap(); + module_g.module_type = ModuleType::Js; + module_g.meta = ModuleMetaData::Script(ScriptModuleMetaData { + hmr_self_accepted: false, + ..Default::default() + }); + + let context = create_context(module_graph); + let boundaries = find_hmr_boundaries(&vec!["G".into()], &context); + // Be careful, the order of the paths may not be guaranteed. check the order if the test fails. + assert_eq!( + boundaries, + vec![("G".into(), vec![vec!["G".into(), "E".into(), "B".into()]])] + .into_iter() + .collect::>() + ); + } } diff --git a/crates/compiler/src/update/mod.rs b/crates/compiler/src/update/mod.rs index 218950ccb..bc7f23bf8 100644 --- a/crates/compiler/src/update/mod.rs +++ b/crates/compiler/src/update/mod.rs @@ -255,7 +255,7 @@ impl Compiler { render_and_generate_update_resource(&updated_module_ids, &diff_result, &self.context)? }; - // find the boundaries. TODO: detect the boundaries in the client side. + // find the boundaries. let boundaries = find_hmr_boundaries::find_hmr_boundaries(&updated_module_ids, &self.context); // TODO: support sourcemap for hmr. and should generate the hmr update response body in rust side. diff --git a/crates/compiler/tests/fixtures/script/accept_deps/bar.js b/crates/compiler/tests/fixtures/script/accept_deps/bar.js new file mode 100644 index 000000000..6cb3174d0 --- /dev/null +++ b/crates/compiler/tests/fixtures/script/accept_deps/bar.js @@ -0,0 +1,3 @@ +export function bar() { + return 'bar'; +} \ No newline at end of file diff --git a/crates/compiler/tests/fixtures/script/accept_deps/dep.ts b/crates/compiler/tests/fixtures/script/accept_deps/dep.ts new file mode 100644 index 000000000..9b1a01acf --- /dev/null +++ b/crates/compiler/tests/fixtures/script/accept_deps/dep.ts @@ -0,0 +1,15 @@ +import { foo } from './foo.js' +import { bar } from './bar.js' + +foo() +bar() + +// Can also accept an array of dep modules: +import.meta.hot.accept( + ['./foo.js', './bar.js'], + ([newFooModule, newBarModule]) => { + // The callback receives an array where only the updated module is + // non null. If the update was not successful (syntax error for ex.), + // the array is empty + }, +) \ No newline at end of file diff --git a/crates/compiler/tests/fixtures/script/accept_deps/foo.js b/crates/compiler/tests/fixtures/script/accept_deps/foo.js new file mode 100644 index 000000000..b0ed0b088 --- /dev/null +++ b/crates/compiler/tests/fixtures/script/accept_deps/foo.js @@ -0,0 +1,3 @@ +export function foo() { + return 'foo'; +} \ No newline at end of file diff --git a/crates/compiler/tests/fixtures/script/accept_deps/index.ts b/crates/compiler/tests/fixtures/script/accept_deps/index.ts new file mode 100644 index 000000000..46d860e83 --- /dev/null +++ b/crates/compiler/tests/fixtures/script/accept_deps/index.ts @@ -0,0 +1,11 @@ +import { foo } from './foo.js' +import './dep'; + +foo() + +if (import.meta.hot) { + import.meta.hot.accept('./foo.js', (newFoo) => { + // the callback receives the updated './foo.js' module + newFoo?.foo() + }); +} \ No newline at end of file diff --git a/crates/compiler/tests/fixtures/script/accept_deps/output.js b/crates/compiler/tests/fixtures/script/accept_deps/output.js new file mode 100644 index 000000000..acb007833 --- /dev/null +++ b/crates/compiler/tests/fixtures/script/accept_deps/output.js @@ -0,0 +1,88 @@ +//index.js: + (globalThis || window || self || global)['__farm_default_namespace__'] = {__FARM_TARGET_ENV__: 'browser'};(function (modules, entryModule) { + var cache = {}; + + function dynamicRequire(id) { + return Promise.resolve(require(id)); + } + + function require(id) { + if (cache[id]) return cache[id].exports; + + var module = { + id: id, + exports: {} + }; + + modules[id](module, module.exports, require, dynamicRequire); + cache[id] = module; + return module.exports; + } + + require(entryModule); + })({"d2214aaa": function(module, exports, farmRequire, farmDynamicRequire) { + "use strict"; + console.log("runtime/index.js")(globalThis || window || self || global)["__farm_default_namespace__"].__farm_module_system__.setPlugins([]); +},}, "d2214aaa");(function (modules) { + for (var key in modules) { + modules[key].__farm_resource_pot__ = 'index_fb79.js'; + (globalThis || window || self || global)['__farm_default_namespace__'].__farm_module_system__.register(key, modules[key]); + } + })({"05ee5ec7": function(module, exports, farmRequire, farmDynamicRequire) { + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + var _foo = farmRequire("59ebf907"); + var _bar = farmRequire("e185e932"); + (0, _foo.foo)(); + (0, _bar.bar)(); + module.meta.hot.accept([ + "foo.js", + "bar.js" + ], ([newFooModule, newBarModule])=>{}); +}, +"59ebf907": function(module, exports, farmRequire, farmDynamicRequire) { + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + Object.defineProperty(exports, "foo", { + enumerable: true, + get: function() { + return foo; + } + }); + function foo() { + return "foo"; + } +}, +"b5d64806": function(module, exports, farmRequire, farmDynamicRequire) { + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + var _foo = farmRequire("59ebf907"); + farmRequire("05ee5ec7"); + (0, _foo.foo)(); + if (module.meta.hot) { + module.meta.hot.accept("foo.js", (newFoo)=>{ + newFoo?.foo(); + }); + } +}, +"e185e932": function(module, exports, farmRequire, farmDynamicRequire) { + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + Object.defineProperty(exports, "bar", { + enumerable: true, + get: function() { + return bar; + } + }); + function bar() { + return "bar"; + } +},});(globalThis || window || self || global)['__farm_default_namespace__'].__farm_module_system__.setInitialLoadedResources([]);(globalThis || window || self || global)['__farm_default_namespace__'].__farm_module_system__.setDynamicModuleResourcesMap({ });var farmModuleSystem = (globalThis || window || self || global)['__farm_default_namespace__'].__farm_module_system__;farmModuleSystem.bootstrap();var entry = farmModuleSystem.require("b5d64806"); \ No newline at end of file diff --git a/crates/compiler/tests/fixtures/update/basic/update1.output.js b/crates/compiler/tests/fixtures/update/basic/update1.output.js index e40580f27..673465350 100644 --- a/crates/compiler/tests/fixtures/update/basic/update1.output.js +++ b/crates/compiler/tests/fixtures/update/basic/update1.output.js @@ -14,9 +14,11 @@ } else { document.head.appendChild(style); } - module.meta.hot.accept(); - module.onDispose(()=>{ - style.remove(); - }); + if (module.meta.hot) { + module.meta.hot.accept(); + module.meta.hot.prune(()=>{ + style.remove(); + }); + } },}) {} \ No newline at end of file diff --git a/crates/compiler/tests/fixtures/update/basic/update2.output.js b/crates/compiler/tests/fixtures/update/basic/update2.output.js index e40580f27..673465350 100644 --- a/crates/compiler/tests/fixtures/update/basic/update2.output.js +++ b/crates/compiler/tests/fixtures/update/basic/update2.output.js @@ -14,9 +14,11 @@ } else { document.head.appendChild(style); } - module.meta.hot.accept(); - module.onDispose(()=>{ - style.remove(); - }); + if (module.meta.hot) { + module.meta.hot.accept(); + module.meta.hot.prune(()=>{ + style.remove(); + }); + } },}) {} \ No newline at end of file diff --git a/crates/compiler/tests/fixtures/update/css-modules/update1.output.js b/crates/compiler/tests/fixtures/update/css-modules/update1.output.js index f8bed5c61..b9631eaa2 100644 --- a/crates/compiler/tests/fixtures/update/css-modules/update1.output.js +++ b/crates/compiler/tests/fixtures/update/css-modules/update1.output.js @@ -30,10 +30,12 @@ } else { document.head.appendChild(style); } - module.meta.hot.accept(); - module.onDispose(()=>{ - style.remove(); - }); + if (module.meta.hot) { + module.meta.hot.accept(); + module.meta.hot.prune(()=>{ + style.remove(); + }); + } }, "index.ts": function(module, exports, farmRequire, farmDynamicRequire) { "use strict"; diff --git a/crates/core/src/cache/cache_store.rs b/crates/core/src/cache/cache_store.rs index f8ce819ae..127a9cd6a 100644 --- a/crates/core/src/cache/cache_store.rs +++ b/crates/core/src/cache/cache_store.rs @@ -9,7 +9,7 @@ use std::{ use crate::config::Mode; -const FARM_CACHE_VERSION: &str = "0.1.2"; +const FARM_CACHE_VERSION: &str = "0.1.3"; const FARM_CACHE_MANIFEST_FILE: &str = "farm-cache.json"; // TODO make CacheStore a trait and implement DiskCacheStore or RemoteCacheStore or more. diff --git a/crates/core/src/module/mod.rs b/crates/core/src/module/mod.rs index 25a1d4a94..06f90798a 100644 --- a/crates/core/src/module/mod.rs +++ b/crates/core/src/module/mod.rs @@ -223,8 +223,9 @@ pub struct ScriptModuleMetaData { pub top_level_mark: u32, pub unresolved_mark: u32, pub module_system: ModuleSystem, - /// true if this module calls `import.meta.hot.accept` - pub hmr_accepted: bool, + /// true if this module calls `import.meta.hot.accept()` or `import.meta.hot.accept(mod => {})` + pub hmr_self_accepted: bool, + pub hmr_accepted_deps: HashSet, } impl Default for ScriptModuleMetaData { @@ -238,7 +239,8 @@ impl Default for ScriptModuleMetaData { top_level_mark: 0, unresolved_mark: 0, module_system: ModuleSystem::EsModule, - hmr_accepted: false, + hmr_self_accepted: false, + hmr_accepted_deps: Default::default(), } } } diff --git a/crates/core/src/plugin/mod.rs b/crates/core/src/plugin/mod.rs index 002abf2b7..ce555a543 100644 --- a/crates/core/src/plugin/mod.rs +++ b/crates/core/src/plugin/mod.rs @@ -537,6 +537,7 @@ impl ChunkResourceInfo { #[serde(rename_all = "camelCase")] pub struct PluginRenderResourcePotHookParam { pub content: Arc, + pub source_map_chain: Vec>, pub resource_pot_info: ChunkResourceInfo, } diff --git a/crates/core/src/plugin/plugin_driver.rs b/crates/core/src/plugin/plugin_driver.rs index ad4c5307d..dc4261782 100644 --- a/crates/core/src/plugin/plugin_driver.rs +++ b/crates/core/src/plugin/plugin_driver.rs @@ -439,23 +439,21 @@ impl PluginDriver { param: &mut PluginRenderResourcePotHookParam, context: &Arc, ) -> Result { - let mut result = PluginDriverRenderResourcePotHookResult { - content: Arc::new(String::new()), - source_map_chain: vec![], - }; - for plugin in &self.plugins { // if the transform hook returns None, treat it as empty hook and ignore it if let Some(plugin_result) = plugin.render_resource_pot(param, context)? { param.content = Arc::new(plugin_result.content); if let Some(source_map) = plugin_result.source_map { - result.source_map_chain.push(Arc::new(source_map)); + param.source_map_chain.push(Arc::new(source_map)); } } } - result.content = param.content.clone(); + let result = PluginDriverRenderResourcePotHookResult { + content: param.content.clone(), + source_map_chain: param.source_map_chain.clone(), + }; Ok(result) } diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 1d6c04b08..3791646a8 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -366,6 +366,24 @@ impl JsCompiler { || !module_ids_by_file.is_empty() } + #[napi] + pub fn get_parent_files(&self, resolved_path: String) -> Vec { + let context = self.compiler.context(); + let module_graph = context.module_graph.read(); + let path = Path::new(&resolved_path); + let module_id = if path.is_absolute() { + ModuleId::from_resolved_path_with_query(&resolved_path, &context.config.root) + } else { + resolved_path.into() + }; + let parents = module_graph.dependents_ids(&module_id); + + parents + .into_iter() + .map(|p| p.resolved_path_with_query(&context.config.root)) + .collect() + } + #[napi] pub fn resources(&self) -> HashMap { let context = self.compiler.context(); diff --git a/crates/plugin_css/src/transform_css_to_script.rs b/crates/plugin_css/src/transform_css_to_script.rs index b068fa0ec..2c35d2258 100644 --- a/crates/plugin_css/src/transform_css_to_script.rs +++ b/crates/plugin_css/src/transform_css_to_script.rs @@ -153,7 +153,8 @@ pub fn transform_css_to_script_modules( top_level_mark: top_level_mark.as_u32(), unresolved_mark: unresolved_mark.as_u32(), module_system: ModuleSystem::EsModule, - hmr_accepted: true, + hmr_self_accepted: true, + hmr_accepted_deps: Default::default(), }); module.module_type = ModuleType::Js; @@ -228,11 +229,13 @@ previousStyle.replaceWith(style); }} else {{ document.head.appendChild(style); }} -module.meta.hot.accept(); -module.onDispose(() => {{ -style.remove(); -}}); +if (module.meta.hot) {{ + module.meta.hot.accept(); + module.meta.hot.prune(() => {{ + style.remove(); + }}); +}} "#, format!( "{}\n{}", diff --git a/crates/plugin_script/Cargo.toml b/crates/plugin_script/Cargo.toml index 131ac103b..4539163ac 100644 --- a/crates/plugin_script/Cargo.toml +++ b/crates/plugin_script/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] farmfe_core = { path = "../core" } farmfe_toolkit = { path = "../toolkit" } +farmfe_utils = { path = "../utils" } farmfe_testing_helpers = { path = "../testing_helpers" } farmfe_swc_transformer_import_glob = { path = "../swc_transformer_import_glob" } swc_plugin_proxy = { version = "0.39.10", features = [ diff --git a/crates/plugin_script/src/import_meta_visitor.rs b/crates/plugin_script/src/import_meta_visitor.rs index bc6e8afc0..58f890048 100644 --- a/crates/plugin_script/src/import_meta_visitor.rs +++ b/crates/plugin_script/src/import_meta_visitor.rs @@ -1,8 +1,16 @@ +use std::{collections::HashSet, sync::Arc}; + use farmfe_core::{ + context::CompilationContext, + module::ModuleId, + plugin::{PluginResolveHookParam, ResolveKind}, swc_common::DUMMY_SP, - swc_ecma_ast::{CallExpr, Callee, Expr, Ident, MemberExpr, MemberProp, MetaPropKind}, + swc_ecma_ast::{ + CallExpr, Callee, Expr, ExprOrSpread, Ident, Lit, MemberExpr, MemberProp, MetaPropKind, Str, + }, }; use farmfe_toolkit::swc_ecma_visit::{VisitMut, VisitMutWith}; +use farmfe_utils::stringify_query; /// transform `import.meta.xxx` to `module.meta.xxx` pub struct ImportMetaVisitor {} @@ -34,20 +42,27 @@ impl VisitMut for ImportMetaVisitor { } pub struct HmrAcceptedVisitor { - pub is_hmr_accepted: bool, + pub is_hmr_self_accepted: bool, + pub hmr_accepted_deps: HashSet, + + module_id: ModuleId, + context: Arc, } impl HmrAcceptedVisitor { - pub fn new() -> Self { + pub fn new(module_id: ModuleId, context: Arc) -> Self { Self { - is_hmr_accepted: false, + is_hmr_self_accepted: false, + hmr_accepted_deps: HashSet::new(), + module_id, + context, } } } impl VisitMut for HmrAcceptedVisitor { fn visit_mut_expr(&mut self, expr: &mut Expr) { - // detect hmr based on `module.meta.hot.accept()` + // detect hmr based on `module.meta.hot.accept` if let Expr::Call(CallExpr { callee: Callee::Expr(box Expr::Member(MemberExpr { @@ -65,6 +80,7 @@ impl VisitMut for HmrAcceptedVisitor { prop: MemberProp::Ident(Ident { sym: accept, .. }), .. })), + args, .. }) = expr { @@ -73,7 +89,65 @@ impl VisitMut for HmrAcceptedVisitor { && &hot.to_string() == "hot" && &accept.to_string() == "accept" { - self.is_hmr_accepted = true; + // if args is empty or the first arg is a function expression, then it's hmr self accepted + if args.is_empty() + || matches!(args[0], ExprOrSpread { + expr: box Expr::Fn(..) | box Expr::Arrow(..), .. + }) + { + self.is_hmr_self_accepted = true; + } else if !args.is_empty() { + let mut resolve_and_replace_deps = |s: &mut Str| { + // string literal + let resolve_result = self.context.plugin_driver.resolve( + &PluginResolveHookParam { + source: s.value.to_string(), + importer: Some(self.module_id.clone()), + kind: ResolveKind::Import, + }, + &self.context, + &Default::default(), + ); + if let Ok(resolved) = resolve_result { + if let Some(resolved) = resolved { + let id = ModuleId::new( + &resolved.resolved_path, + &stringify_query(&resolved.query), + &self.context.config.root, + ); + self.hmr_accepted_deps.insert(id.clone()); + *s = Str { + span: DUMMY_SP, + value: id.to_string().into(), + raw: None, + } + } + } + }; + // if args is not empty and the first arg is a literal, then it's hmr accepted deps + if let ExprOrSpread { + expr: box Expr::Lit(Lit::Str(s)), + .. + } = &mut args[0] + { + resolve_and_replace_deps(s); + } else if let ExprOrSpread { + expr: box Expr::Array(arr), + .. + } = &mut args[0] + { + // array literal + for expr in arr.elems.iter_mut() { + if let Some(ExprOrSpread { + expr: box Expr::Lit(Lit::Str(s)), + .. + }) = expr + { + resolve_and_replace_deps(s); + } + } + } + } } } else { expr.visit_mut_children_with(self); diff --git a/crates/plugin_script/src/lib.rs b/crates/plugin_script/src/lib.rs index 84b98f6f6..47a55d424 100644 --- a/crates/plugin_script/src/lib.rs +++ b/crates/plugin_script/src/lib.rs @@ -1,7 +1,11 @@ #![feature(box_patterns)] #![feature(path_file_prefix)] -use std::{path::PathBuf, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, + sync::Arc, +}; use deps_analyzer::DepsAnalyzer; use farmfe_core::{ @@ -15,6 +19,7 @@ use farmfe_core::{ PluginGenerateResourcesHookResult, PluginHookContext, PluginLoadHookParam, PluginLoadHookResult, PluginParseHookParam, PluginProcessModuleHookParam, }, + rayon::iter::{IntoParallelIterator, ParallelIterator}, resource::{ resource_pot::{ResourcePot, ResourcePotType}, Resource, ResourceOrigin, ResourceType, @@ -114,8 +119,8 @@ impl Plugin for FarmPluginScript { unresolved_mark: unresolved_mark.as_u32(), // set module_system to unknown, it will be detected in `finalize_module` module_system: ModuleSystem::Custom(String::from("unknown")), - // set module_type to unknown, it will be detected in `finalize_module` - hmr_accepted: false, + hmr_self_accepted: false, + hmr_accepted_deps: Default::default(), }; Ok(Some(ModuleMetaData::Script(meta))) @@ -238,21 +243,28 @@ impl Plugin for FarmPluginScript { } } + // find and replace `import.meta.xxx` to `module.meta.xxx` and detect hmr_accepted // skip transform import.meta when targetEnv is node - if matches!(context.config.output.target_env, TargetEnv::Browser) { + if matches!(context.config.output.target_env, TargetEnv::Browser) + || matches!(context.config.output.format, ModuleFormat::CommonJs) + { // transform `import.meta.xxx` to `module.meta.xxx` let ast = &mut param.module.meta.as_script_mut().ast; let mut import_meta_v = ImportMetaVisitor::new(); ast.visit_mut_with(&mut import_meta_v); + } - let mut hmr_accepted_v = import_meta_visitor::HmrAcceptedVisitor::new(); - ast.visit_mut_with(&mut hmr_accepted_v); - param.module.meta.as_script_mut().hmr_accepted = hmr_accepted_v.is_hmr_accepted; - } else if matches!(context.config.output.format, ModuleFormat::CommonJs) { - // transform `import.meta.xxx` to `module.meta.xxx` + if matches!(context.config.output.target_env, TargetEnv::Browser) { let ast = &mut param.module.meta.as_script_mut().ast; - let mut import_meta_v = ImportMetaVisitor::new(); - ast.visit_mut_with(&mut import_meta_v); + let mut hmr_accepted_v = + import_meta_visitor::HmrAcceptedVisitor::new(param.module.id.clone(), context.clone()); + ast.visit_mut_with(&mut hmr_accepted_v); + param.module.meta.as_script_mut().hmr_self_accepted = hmr_accepted_v.is_hmr_self_accepted; + param.module.meta.as_script_mut().hmr_accepted_deps = hmr_accepted_v + .hmr_accepted_deps + .into_iter() + .map(|dep| dep.into()) + .collect(); } Ok(None) @@ -273,7 +285,7 @@ impl Plugin for FarmPluginScript { emitted: false, resource_type: ResourceType::Js, origin: ResourceOrigin::ResourcePot(resource_pot.id.clone()), - info: None + info: None, }; let mut source_map = None; @@ -298,7 +310,7 @@ impl Plugin for FarmPluginScript { emitted: false, resource_type: ResourceType::SourceMap(resource_pot.id.to_string()), origin: ResourceOrigin::ResourcePot(resource_pot.id.clone()), - info: None + info: None, }; source_map = Some(map); diff --git a/crates/plugin_script/tests/fixtures/hmr_accepted/accept_with_args.ts b/crates/plugin_script/tests/fixtures/hmr_accepted/accept_with_args.ts index 026579cca..0a468c067 100644 --- a/crates/plugin_script/tests/fixtures/hmr_accepted/accept_with_args.ts +++ b/crates/plugin_script/tests/fixtures/hmr_accepted/accept_with_args.ts @@ -2,6 +2,6 @@ function render(m) { return 'Hello, world!' + m; } -module.meta.hot.accept((module) => { +import.meta.hot.accept((module) => { render(module.default); }); diff --git a/crates/plugin_script/tests/fixtures/hmr_accepted/accept_without_args.ts b/crates/plugin_script/tests/fixtures/hmr_accepted/accept_without_args.ts index f7ec22e20..08744746d 100644 --- a/crates/plugin_script/tests/fixtures/hmr_accepted/accept_without_args.ts +++ b/crates/plugin_script/tests/fixtures/hmr_accepted/accept_without_args.ts @@ -2,4 +2,4 @@ export function render() { return 'Hello, world!'; } -module.meta.hot.accept(); +import.meta.hot.accept(); diff --git a/crates/plugin_script/tests/hmr_accepted.rs b/crates/plugin_script/tests/hmr_accepted.rs index e5d4782b5..37a67f10c 100644 --- a/crates/plugin_script/tests/hmr_accepted.rs +++ b/crates/plugin_script/tests/hmr_accepted.rs @@ -57,7 +57,7 @@ fn hmr_accepted() { module.meta = module_meta; module.module_type = loaded.module_type; - assert!(!module.meta.as_script().hmr_accepted); + assert!(!module.meta.as_script().hmr_self_accepted); plugin_script .finalize_module( &mut PluginFinalizeModuleHookParam { @@ -67,7 +67,6 @@ fn hmr_accepted() { &context, ) .unwrap(); - - assert!(module.meta.as_script().hmr_accepted); + assert!(module.meta.as_script().hmr_self_accepted); }); } diff --git a/crates/plugin_tree_shake/tests/common/mod.rs b/crates/plugin_tree_shake/tests/common/mod.rs index 1cae45dbf..962533106 100644 --- a/crates/plugin_tree_shake/tests/common/mod.rs +++ b/crates/plugin_tree_shake/tests/common/mod.rs @@ -42,7 +42,8 @@ pub fn create_module(code: &str) -> (Module, Arc) { top_level_mark: 0, unresolved_mark: 0, module_system: farmfe_core::module::ModuleSystem::EsModule, - hmr_accepted: false, + hmr_self_accepted: false, + hmr_accepted_deps: Default::default(), }); (module, cm) } @@ -56,7 +57,8 @@ pub fn create_module_with_globals(code: &str) -> Module { top_level_mark: 0, unresolved_mark: 0, module_system: farmfe_core::module::ModuleSystem::EsModule, - hmr_accepted: false, + hmr_self_accepted: false, + hmr_accepted_deps: Default::default(), }); module }) diff --git a/cspell.json b/cspell.json index 87d6f1614..90d135441 100644 --- a/cspell.json +++ b/cspell.json @@ -138,6 +138,7 @@ "**/examples/**", "rust-plugins/sass/ext/sass/**", "assets/**", - "**/*.d.ts" + "**/*.d.ts", + "**/LICENSE" ] } diff --git a/examples/hmr/index.html b/examples/hmr/index.html new file mode 100644 index 000000000..1eded4719 --- /dev/null +++ b/examples/hmr/index.html @@ -0,0 +1,12 @@ + + + + + + Document + + +
+ + + \ No newline at end of file diff --git a/examples/hmr/package.json b/examples/hmr/package.json new file mode 100644 index 000000000..ca13821e9 --- /dev/null +++ b/examples/hmr/package.json @@ -0,0 +1,17 @@ +{ + "name": "@farmfe-examples/hmr", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "farm start", + "build": "farm build", + "preview": "farm preview" + }, + "devDependencies": { + "@farmfe/cli": "workspace:^", + "@farmfe/core": "workspace:^" + }, + "dependencies": { + "core-js": "^3.34.0" + } +} diff --git a/examples/hmr/src/accept-deps-array.ts b/examples/hmr/src/accept-deps-array.ts new file mode 100644 index 000000000..d52018b1b --- /dev/null +++ b/examples/hmr/src/accept-deps-array.ts @@ -0,0 +1,27 @@ +import { compData } from "./accept-deps-data" + +const id = "AcceptDepsArray"; + +export function AcceptDepsArray() { + return { + render: () => { + const renderData = id + ":" + compData(id); + + const div = document.createElement("div", {}); + div.id = id; + div.innerText = renderData; + div.className = "box"; + return div; + } + } +} + +if (import.meta.hot) { + // accept dependencies + import.meta.hot.accept(['./accept-deps-data'], ([data]) => { + console.log(data); + const div = document.getElementById(id); + const renderData = data.compData(id); + div!.innerText = renderData; + }); +} \ No newline at end of file diff --git a/examples/hmr/src/accept-deps-data.ts b/examples/hmr/src/accept-deps-data.ts new file mode 100644 index 000000000..5f7533c66 --- /dev/null +++ b/examples/hmr/src/accept-deps-data.ts @@ -0,0 +1,3 @@ +export function compData(id: string) { + return `${id} : comp-data`; +} \ No newline at end of file diff --git a/examples/hmr/src/accept-deps-string.ts b/examples/hmr/src/accept-deps-string.ts new file mode 100644 index 000000000..0e4d76170 --- /dev/null +++ b/examples/hmr/src/accept-deps-string.ts @@ -0,0 +1,27 @@ +import { compData } from "./accept-deps-data" + +const id = "AcceptDepsString"; + +export function AcceptDepsString() { + return { + render: () => { + const renderData = compData(id); + + const div = document.createElement("div", {}); + div.id = id; + div.innerText = renderData; + div.className = "box"; + return div; + } + } +} + +if (import.meta.hot) { + // accept dependencies + import.meta.hot.accept('./accept-deps-data', (data) => { + console.log(data); + const div = document.getElementById(id); + const renderData = data.compData(id); + div!.innerText = renderData; + }); +} \ No newline at end of file diff --git a/examples/hmr/src/data.ts b/examples/hmr/src/data.ts new file mode 100644 index 000000000..52eaebca0 --- /dev/null +++ b/examples/hmr/src/data.ts @@ -0,0 +1,3 @@ +export function data() { + return 'data'; +} \ No newline at end of file diff --git a/examples/hmr/src/dispose.ts b/examples/hmr/src/dispose.ts new file mode 100644 index 000000000..6b455b2ac --- /dev/null +++ b/examples/hmr/src/dispose.ts @@ -0,0 +1,40 @@ +const id = "Dispose"; + +export function createChild() { + const child = document.createElement("div", {}); + child.innerText = id + new Date(); + child.className = "box"; + return child; +} + + +export function Dispose() { + return { + render: () => { + const div = document.createElement("div", {}); + div.id = id; + const child = createChild(); + div.appendChild(child); + return div; + } + } +} + +if (import.meta.hot) { + // self accept without reload the page + import.meta.hot.accept(mod => { + const div = document.getElementById(id); + div?.appendChild(mod.createChild()); + }); + import.meta.hot.dispose(() => { + // remove all children of the div + const div = document.getElementById(id); + + if (div) { + while (div.firstChild) { + console.log('dispose', div.firstChild); + div.removeChild(div.firstChild); + } + } + }); +} \ No newline at end of file diff --git a/examples/hmr/src/event.ts b/examples/hmr/src/event.ts new file mode 100644 index 000000000..e69de29bb diff --git a/examples/hmr/src/index.ts b/examples/hmr/src/index.ts new file mode 100644 index 000000000..524c72b01 --- /dev/null +++ b/examples/hmr/src/index.ts @@ -0,0 +1,44 @@ +// self accept without reload the page +import { data } from "./data"; +import { AcceptDepsString } from "./accept-deps-string"; +import { AcceptDepsArray } from "./accept-deps-array"; +import { SelfAcceptedEmpty } from "./self-accepted-empty"; +import { SelfAcceptedFn } from "./self-accepted-fn"; +import { InvalidateParent } from "./invalidate-parent"; +import { Dispose } from "./dispose"; + +import './prune'; + +function render() { + const root = document.getElementById("root"); + // remove all children of root + root!.innerHTML = ""; + + const renderData = data(); + const div = document.createElement("div"); + div.id = "root-comp"; + div.innerText = renderData; + root?.appendChild(div); + + const comps = [ + AcceptDepsArray(), + AcceptDepsString(), + SelfAcceptedEmpty(), + SelfAcceptedFn(), + InvalidateParent(), + Dispose() + ]; + + comps.forEach(comp => { + root?.appendChild(comp.render()); + }); +} + +render(); + +if (import.meta.hot) { + // self accept without reload the page + import.meta.hot.accept(); + + render(); +} \ No newline at end of file diff --git a/examples/hmr/src/invalidate-parent.ts b/examples/hmr/src/invalidate-parent.ts new file mode 100644 index 000000000..4e060eac8 --- /dev/null +++ b/examples/hmr/src/invalidate-parent.ts @@ -0,0 +1,29 @@ +import { invalidate } from "./invalidate" + +const id = "InvalidateParent"; + +export function InvalidateParent() { + return { + render: () => { + const renderData = invalidate(); + + const div = document.createElement("div", {}); + div.id = id; + div.innerText = renderData; + div.className = "box"; + return div; + } + } +} + +if (import.meta.hot) { + // self accept without reload the page + import.meta.hot.accept(); + const div = document.getElementById(id); + + if (div) { + const comp = InvalidateParent().render(); + console.log(div, comp); + div.replaceWith(comp); + } +} \ No newline at end of file diff --git a/examples/hmr/src/invalidate.ts b/examples/hmr/src/invalidate.ts new file mode 100644 index 000000000..2d87c64a3 --- /dev/null +++ b/examples/hmr/src/invalidate.ts @@ -0,0 +1,12 @@ +import { data } from "./data"; + +export function invalidate() { + return `invalidate: ${data()}`; +} + +if (import.meta.hot) { + // accept dependencies + import.meta.hot.accept(() => { + import.meta.hot.invalidate('parent module should accept this'); + }); +} \ No newline at end of file diff --git a/examples/hmr/src/prune.ts b/examples/hmr/src/prune.ts new file mode 100644 index 000000000..b2edfe65c --- /dev/null +++ b/examples/hmr/src/prune.ts @@ -0,0 +1,30 @@ +const id = 'comp-style'; +const style = document.createElement('style'); +style.id = id; +console.log('style', style); +style.innerHTML = ` + .box { + margin-top: 20px; + width: fit-content; + height: 50px; + background-color: purple; + line-height: 50px; + padding: 15px; + color: white; + } +`; + +const existingStyle = document.getElementById(id); +if (existingStyle) { + existingStyle.innerHTML = style.innerHTML; +} else { + document.head.appendChild(style); +} + +if (import.meta.hot) { + import.meta.hot.accept(); + import.meta.hot.prune(() => { + const style = document.getElementById(id) + style?.remove() + }) +} \ No newline at end of file diff --git a/examples/hmr/src/self-accepted-empty.ts b/examples/hmr/src/self-accepted-empty.ts new file mode 100644 index 000000000..c91a87cb0 --- /dev/null +++ b/examples/hmr/src/self-accepted-empty.ts @@ -0,0 +1,30 @@ +import { compData } from "./accept-deps-data" + +const id = "SelfAcceptedEmpty"; + +export function SelfAcceptedEmpty() { + return { + render: () => { + const renderData = compData(id); + + const div = document.createElement("div", {}); + div.id = id; + div.innerText = renderData; + div.className = "box"; + return div; + } + } +} + +if (import.meta.hot) { + // self accept without reload the page + import.meta.hot.accept(); + + const div = document.getElementById(id); + + if (div) { + const comp = SelfAcceptedEmpty().render(); + console.log(div, comp); + div.replaceWith(comp); + } +} \ No newline at end of file diff --git a/examples/hmr/src/self-accepted-fn.ts b/examples/hmr/src/self-accepted-fn.ts new file mode 100644 index 000000000..ba8100b1b --- /dev/null +++ b/examples/hmr/src/self-accepted-fn.ts @@ -0,0 +1,28 @@ +import { compData } from "./accept-deps-data" + +const id = "SelfAcceptedFn"; + +export function SelfAcceptedFn() { + return { + render: () => { + const renderData = compData(id); + + const div = document.createElement("div", {}); + div.id = id; + div.innerText = renderData; + div.className = "box"; + return div; + } + } +} + +if (import.meta.hot) { + // self accept without reload the page + import.meta.hot.accept(mod => { + const div = document.getElementById(id); + console.log('hot self accept', mod, div) + const comp = mod[id]().render(); + console.log(div, comp); + div?.replaceWith(comp); + }); +} \ No newline at end of file diff --git a/examples/vite-adapter-svelet/.gitignore b/examples/vite-adapter-svelet/.gitignore new file mode 100644 index 000000000..09b661bfb --- /dev/null +++ b/examples/vite-adapter-svelet/.gitignore @@ -0,0 +1 @@ +.svelte-kit \ No newline at end of file diff --git a/examples/vite-adapter-svelet/farm.config.ts b/examples/vite-adapter-svelet/farm.config.ts new file mode 100644 index 000000000..d6620dd63 --- /dev/null +++ b/examples/vite-adapter-svelet/farm.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from '@farmfe/core'; + +export default defineConfig({ + vitePlugins: [sveltekit()] +}); diff --git a/examples/vite-adapter-svelet/package.json b/examples/vite-adapter-svelet/package.json new file mode 100644 index 000000000..89c6fdb8a --- /dev/null +++ b/examples/vite-adapter-svelet/package.json @@ -0,0 +1,23 @@ +{ + "name": "@farmfe-examples/svelet", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "farm dev", + "build": "echo \"Error: build script not supported\"", + "preview": "farm preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@farmfe/cli": "workspace:^", + "@farmfe/core": "workspace:^", + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/kit": "^1.27.4", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "tslib": "^2.4.1", + "typescript": "^5.0.0" + }, + "type": "module" +} diff --git a/examples/vite-adapter-svelet/src/app.d.ts b/examples/vite-adapter-svelet/src/app.d.ts new file mode 100644 index 000000000..f59b884c5 --- /dev/null +++ b/examples/vite-adapter-svelet/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/examples/vite-adapter-svelet/src/app.html b/examples/vite-adapter-svelet/src/app.html new file mode 100644 index 000000000..77a5ff52c --- /dev/null +++ b/examples/vite-adapter-svelet/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/vite-adapter-svelet/src/lib/index.ts b/examples/vite-adapter-svelet/src/lib/index.ts new file mode 100644 index 000000000..856f2b6c3 --- /dev/null +++ b/examples/vite-adapter-svelet/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/examples/vite-adapter-svelet/src/routes/+page.svelte b/examples/vite-adapter-svelet/src/routes/+page.svelte new file mode 100644 index 000000000..5982b0ae3 --- /dev/null +++ b/examples/vite-adapter-svelet/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit kit.svelte.dev to read the documentation

diff --git a/examples/vite-adapter-svelet/static/favicon.png b/examples/vite-adapter-svelet/static/favicon.png new file mode 100644 index 000000000..825b9e65a Binary files /dev/null and b/examples/vite-adapter-svelet/static/favicon.png differ diff --git a/examples/vite-adapter-svelet/svelte.config.js b/examples/vite-adapter-svelet/svelte.config.js new file mode 100644 index 000000000..1cf26a00d --- /dev/null +++ b/examples/vite-adapter-svelet/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/kit/vite'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/examples/vite-adapter-svelet/vite.config.ts b/examples/vite-adapter-svelet/vite.config.ts new file mode 100644 index 000000000..bbf8c7da4 --- /dev/null +++ b/examples/vite-adapter-svelet/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/js-plugins/dts/src/index.ts b/js-plugins/dts/src/index.ts index 4ce110845..5a88a9983 100644 --- a/js-plugins/dts/src/index.ts +++ b/js-plugins/dts/src/index.ts @@ -9,12 +9,11 @@ import type { DtsPluginOptions } from './types.js'; export default function farmDtsPlugin(options: DtsPluginOptions): JsPlugin { const ctx = new Context(); // TODO support vue other framework file type - // TODO support alias return { name: pluginName, priority: 1000, - configResolved(config: any) { - ctx.handleResolveOptions(options, config); + configResolved(config) { + ctx.handleResolveOptions(options, config.compilation); }, load: { filters: { diff --git a/js-plugins/less/farm.config.mjs b/js-plugins/less/farm.config.mjs index b652dac25..b80160c9d 100644 --- a/js-plugins/less/farm.config.mjs +++ b/js-plugins/less/farm.config.mjs @@ -1,43 +1,9 @@ -import { builtinModules } from 'module'; + import farmDtsPlugin from '@farmfe/js-plugin-dts'; +import { createFarmJsPluginBuildConfig } from '../../configs/farm-js-plugin.base.config.mjs'; -/** - * @type {import('@farmfe/core').UserConfig} - */ -export default { - compilation: { - input: { - index: './src/index.ts' - }, - output: { - path: 'build/' + (process.env.FARM_FORMAT || 'cjs'), - entryFilename: - '[entryName].' + (process.env.FARM_FORMAT === 'esm' ? 'js' : 'cjs'), - targetEnv: 'node', - format: process.env.FARM_FORMAT || 'cjs' - }, - external: [ - ...builtinModules.map((m) => `^${m}$`), - ...builtinModules.map((m) => `^node:${m}$`) - ], - partialBundling: { - enforceResources: [ - { - name: 'index.js', - test: ['.+'] - } - ] - }, - minify: false, - sourcemap: false, - presetEnv: false - }, - server: { - hmr: false - }, - plugins: [ - farmDtsPlugin({ - tsConfigPath: './tsconfig.build.json' - }) - ] -}; +export default createFarmJsPluginBuildConfig([ + farmDtsPlugin({ + tsConfigPath: './tsconfig.build.json' + }) +]); diff --git a/js-plugins/less/package.json b/js-plugins/less/package.json index d9750d69b..13f0cd3f2 100644 --- a/js-plugins/less/package.json +++ b/js-plugins/less/package.json @@ -7,7 +7,7 @@ "type": "module", "exports": { ".": { - "import": "./build/esm/index.js", + "import": "./build/esm/index.mjs", "types": "./build/cjs/index.d.ts", "require": "./build/cjs/index.cjs", "default": "./build/cjs/index.cjs" diff --git a/js-plugins/less/src/index.ts b/js-plugins/less/src/index.ts index 90c67240f..230419d38 100644 --- a/js-plugins/less/src/index.ts +++ b/js-plugins/less/src/index.ts @@ -1,4 +1,4 @@ -import { DevServer, JsPlugin, UserConfig } from '@farmfe/core'; +import { Compiler, JsPlugin, UserConfig } from '@farmfe/core'; import { getLessImplementation, pluginName, @@ -24,7 +24,7 @@ export default function farmLessPlugin( options: LessPluginOptions = {} ): JsPlugin { let farmConfig: UserConfig['compilation']; - let devServer: DevServer; + let compiler: Compiler; const implementation: LessStatic = getLessImplementation( options?.implementation ); @@ -32,10 +32,17 @@ export default function farmLessPlugin( return { name: pluginName, configResolved: (config) => { - farmConfig = config; + farmConfig = config.compilation; }, - configDevServer(server) { - devServer = server; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore it will be removed in the future + configDevServer() { + console.warn( + '[@farmfe/js-plugin-less] Your plugin version is not compatible with the current farm version, please update @farmfe/core to the latest version, otherwise the plugin may not work properly.' + ); + }, + configureCompiler(c) { + compiler = c; }, load: { filters: { @@ -94,10 +101,9 @@ export default function farmLessPlugin( paths: configPaths ? [fileRoot, ...configPaths] : [fileRoot] } as Less.Options); - if (devServer && imports && !isProd) { + if (compiler && imports && !isProd) { for (const dep of imports) { - // TODO add a compilerCreated hook to farmfe/core and get the compiler instead of using devServer - devServer.addWatchFile(param.resolvedPath, [ + compiler.addExtraWatchFile(param.resolvedPath, [ path.resolve(fileRoot, dep) ]); } diff --git a/js-plugins/postcss/farm.config.mjs b/js-plugins/postcss/farm.config.mjs index 3b6c465b1..8c929d79c 100644 --- a/js-plugins/postcss/farm.config.mjs +++ b/js-plugins/postcss/farm.config.mjs @@ -1,39 +1,12 @@ import farmDtsPlugin from '@farmfe/js-plugin-dts'; +import { createFarmJsPluginBuildConfig } from '../../configs/farm-js-plugin.base.config.mjs'; /** * @type {import('@farmfe/core').UserConfig} */ -export default { - compilation: { - input: { - index: './src/index.ts' - }, - output: { - path: 'build/' + (process.env.FARM_FORMAT || 'cjs'), - entryFilename: - '[entryName].' + (process.env.FARM_FORMAT === 'esm' ? 'js' : 'cjs'), - filename: '[resourceName].[contentHash].cjs', - targetEnv: 'node', - format: process.env.FARM_FORMAT || 'cjs' - }, - partialBundling: { - enforceResources: [ - { - name: 'index.js', - test: ['.+'] - } - ] - }, - minify: false, - sourcemap: false, - presetEnv: false - }, - server: { - hmr: false - }, - plugins: [ - farmDtsPlugin({ - tsConfigPath: './tsconfig.build.json' - }) - ] -}; +export default createFarmJsPluginBuildConfig([ + farmDtsPlugin({ + tsConfigPath: './tsconfig.build.json' + }) +] +); diff --git a/js-plugins/postcss/package.json b/js-plugins/postcss/package.json index 88ffb70f9..3fdc75e76 100644 --- a/js-plugins/postcss/package.json +++ b/js-plugins/postcss/package.json @@ -7,7 +7,7 @@ "type": "module", "exports": { ".": { - "import": "./build/esm/index.js", + "import": "./build/esm/index.mjs", "types": "./build/cjs/index.d.ts", "require": "./build/cjs/index.cjs", "default": "./build/cjs/index.cjs" diff --git a/js-plugins/postcss/src/index.ts b/js-plugins/postcss/src/index.ts index c9ef57fb7..2217f05fc 100644 --- a/js-plugins/postcss/src/index.ts +++ b/js-plugins/postcss/src/index.ts @@ -1,4 +1,4 @@ -import { JsPlugin, UserConfig } from '@farmfe/core'; +import { JsPlugin, ResolvedUserConfig } from '@farmfe/core'; import postcssLoadConfig from 'postcss-load-config'; import { ProcessOptions, Processor } from 'postcss'; import path from 'path'; @@ -35,7 +35,7 @@ export default function farmPostcssPlugin( // Execute last priority: 0, - configResolved: async (config: UserConfig) => { + configResolved: async (config: ResolvedUserConfig) => { const { plugins, options: _options } = await postcssLoadConfig( options.postcssLoadConfig?.ctx, options.postcssLoadConfig?.path ?? config.root, @@ -43,7 +43,6 @@ export default function farmPostcssPlugin( ); postcssOptions = _options; postcssProcessor = implementation(plugins); - return config; }, transform: { diff --git a/js-plugins/postcss/tsconfig.json b/js-plugins/postcss/tsconfig.json index d3ba2eab2..47544a236 100644 --- a/js-plugins/postcss/tsconfig.json +++ b/js-plugins/postcss/tsconfig.json @@ -1,4 +1,5 @@ { "extends": "../../tsconfig.base.json", + "include": ["src"], "exclude": ["node_modules"] } diff --git a/js-plugins/record-viewer/.eslintrc.json b/js-plugins/record-viewer/.eslintrc.json index 2c3c080c7..c5a96a660 100644 --- a/js-plugins/record-viewer/.eslintrc.json +++ b/js-plugins/record-viewer/.eslintrc.json @@ -1,7 +1,7 @@ { "root": true, + "extends": "../../.eslintrc.base.json", "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" + "project": ["./js-plugins/record-viewer/tsconfig.json"] } } diff --git a/js-plugins/record-viewer/farm.config.ts b/js-plugins/record-viewer/farm.config.ts index 85e14348e..f54af0fc2 100644 --- a/js-plugins/record-viewer/farm.config.ts +++ b/js-plugins/record-viewer/farm.config.ts @@ -1,46 +1,9 @@ -import { builtinModules } from 'module'; -// import type { UserConfig } from '@farmfe/core'; import farmDtsPlugin from '@farmfe/js-plugin-dts'; -import { defineConfig } from '@farmfe/core'; -console.log(farmDtsPlugin, defineConfig); +import { createFarmJsPluginBuildConfig } from '../../configs/farm-js-plugin.base.config.mjs'; -export default defineConfig({ - compilation: { - input: { - index: './src/index.ts' - }, - output: { - path: 'build/' + (process.env.FARM_FORMAT || 'cjs'), - entryFilename: - '[entryName].' + (process.env.FARM_FORMAT === 'esm' ? 'js' : 'cjs'), - targetEnv: 'node', - format: process.env.FARM_FORMAT || 'cjs' - }, - external: [ - ...builtinModules.map((m) => `^${m}$`), - ...builtinModules.map((m) => `^node:${m}$`) - ], - partialBundling: { - enforceResources: [ - { - name: 'index.js', - test: ['.+'] - } - ] - }, - minify: false, - sourcemap: false, - presetEnv: false, - - }, - server: { - hmr: false, - cors: true - }, - plugins: [ - farmDtsPlugin({ - tsConfigPath: './tsconfig.build.json' - }) - ] -}); +export default createFarmJsPluginBuildConfig([ + farmDtsPlugin({ + tsConfigPath: './tsconfig.build.json' + }) +]); diff --git a/js-plugins/record-viewer/package.json b/js-plugins/record-viewer/package.json index 56bda0b76..22dbe8a0e 100644 --- a/js-plugins/record-viewer/package.json +++ b/js-plugins/record-viewer/package.json @@ -6,7 +6,7 @@ "type": "module", "exports": { ".": { - "import": "./build/esm/index.js", + "import": "./build/esm/index.mjs", "types": "./build/cjs/index.d.ts", "require": "./build/cjs/index.cjs", "default": "./build/cjs/index.cjs" @@ -49,4 +49,4 @@ "files": [ "build" ] -} \ No newline at end of file +} diff --git a/js-plugins/record-viewer/src/index.ts b/js-plugins/record-viewer/src/index.ts index 832ae2187..886d4b512 100644 --- a/js-plugins/record-viewer/src/index.ts +++ b/js-plugins/record-viewer/src/index.ts @@ -1,13 +1,11 @@ import { JsPlugin, UserConfig } from '@farmfe/core'; -import { resolve } from 'node:path'; +import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { RecordViewerOptions } from './types'; import { createRecordViewerServer } from './node/server'; import { createDateSourceMiddleware } from './node/dataSource'; -const path = require('path'); - -const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url)); +const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url)); export const PLUGIN_DIR_CLIENT = resolve(PLUGIN_DIR, '../client'); @@ -15,28 +13,16 @@ export default function farmRecorderPlugin( options: RecordViewerOptions = {} ): JsPlugin { let farmConfig: UserConfig['compilation']; - let recordViewerOptions: RecordViewerOptions; + const recordViewerOptions: RecordViewerOptions = options; return { name: 'farm-plugin-record-viewer', - configResolved: (config) => { - farmConfig = config || {}; + config(config) { + farmConfig = config.compilation || {}; farmConfig.record = true; - recordViewerOptions = options; - }, - configDevServer: (devServer) => { - const compiler = devServer.getCompiler(); - - const middleware = createDateSourceMiddleware(compiler); - - createRecordViewerServer({ - host: recordViewerOptions.host, - port: recordViewerOptions.port, - clientPath: PLUGIN_DIR_CLIENT, - middleware - }); + return config; }, - configCompiler: (compiler) => { + configureCompiler: (compiler) => { const middleware = createDateSourceMiddleware(compiler); createRecordViewerServer({ diff --git a/js-plugins/sass/farm.config.mjs b/js-plugins/sass/farm.config.mjs index b652dac25..7d94bbdf6 100644 --- a/js-plugins/sass/farm.config.mjs +++ b/js-plugins/sass/farm.config.mjs @@ -1,43 +1,12 @@ -import { builtinModules } from 'module'; + import farmDtsPlugin from '@farmfe/js-plugin-dts'; +import { createFarmJsPluginBuildConfig } from '../../configs/farm-js-plugin.base.config.mjs'; /** * @type {import('@farmfe/core').UserConfig} */ -export default { - compilation: { - input: { - index: './src/index.ts' - }, - output: { - path: 'build/' + (process.env.FARM_FORMAT || 'cjs'), - entryFilename: - '[entryName].' + (process.env.FARM_FORMAT === 'esm' ? 'js' : 'cjs'), - targetEnv: 'node', - format: process.env.FARM_FORMAT || 'cjs' - }, - external: [ - ...builtinModules.map((m) => `^${m}$`), - ...builtinModules.map((m) => `^node:${m}$`) - ], - partialBundling: { - enforceResources: [ - { - name: 'index.js', - test: ['.+'] - } - ] - }, - minify: false, - sourcemap: false, - presetEnv: false - }, - server: { - hmr: false - }, - plugins: [ - farmDtsPlugin({ - tsConfigPath: './tsconfig.build.json' - }) - ] -}; +export default createFarmJsPluginBuildConfig([ + farmDtsPlugin({ + tsConfigPath: './tsconfig.build.json' + }) +]); diff --git a/js-plugins/sass/package.json b/js-plugins/sass/package.json index 5bbe8f828..76fd44bf5 100644 --- a/js-plugins/sass/package.json +++ b/js-plugins/sass/package.json @@ -7,7 +7,7 @@ "type": "module", "exports": { ".": { - "import": "./build/esm/index.js", + "import": "./build/esm/index.mjs", "types": "./build/cjs/index.d.ts", "require": "./build/cjs/index.cjs", "default": "./build/cjs/index.cjs" diff --git a/js-plugins/sass/src/index.ts b/js-plugins/sass/src/index.ts index d5a12ad5f..bd8fb5f47 100644 --- a/js-plugins/sass/src/index.ts +++ b/js-plugins/sass/src/index.ts @@ -49,7 +49,7 @@ export default function farmSassPlugin( return { name: pluginName, configResolved: (config) => { - farmConfig = config; + farmConfig = config.compilation; }, load: { filters: { resolvedPaths }, diff --git a/js-plugins/solid/farm.config.mjs b/js-plugins/solid/farm.config.mjs index dc22f4ba9..7e6aade54 100644 --- a/js-plugins/solid/farm.config.mjs +++ b/js-plugins/solid/farm.config.mjs @@ -1,44 +1,5 @@ -import { builtinModules } from 'module'; + import farmDtsPlugin from '@farmfe/js-plugin-dts'; +import { createFarmJsPluginBuildConfig } from '../../configs/farm-js-plugin.base.config.mjs'; -/** - * @type {import('@farmfe/core').UserConfig} - */ -export default { - compilation: { - minify: false, - presetEnv: false, - input: { - index: './src/index.ts' - }, - output: { - path: 'build/' + (process.env.FARM_FORMAT || 'cjs'), - entryFilename: - '[entryName].' + (process.env.FARM_FORMAT === 'esm' ? 'js' : 'cjs'), - targetEnv: 'node', - format: process.env.FARM_FORMAT || 'cjs' - }, - external: [ - ...builtinModules.map((m) => `^${m}$`), - ...builtinModules.map((m) => `^node:${m}$`) - ], - partialBundling: { - enforceResources: [ - { - name: 'index.js', - test: ['.+'] - } - ] - }, - sourcemap: false, - persistentCache: { - envs: { - format: process.env.FARM_FORMAT ?? '' - } - } - }, - server: { - hmr: false - }, - plugins: [farmDtsPlugin()] -}; +export default createFarmJsPluginBuildConfig([farmDtsPlugin()]); diff --git a/js-plugins/solid/package.json b/js-plugins/solid/package.json index c5a4cbd91..075861755 100644 --- a/js-plugins/solid/package.json +++ b/js-plugins/solid/package.json @@ -12,7 +12,7 @@ "type": "module", "exports": { ".": { - "import": "./build/esm/index.js", + "import": "./build/esm/index.mjs", "types": "./build/cjs/index.d.ts", "require": "./build/cjs/index.cjs", "default": "./build/cjs/index.cjs" diff --git a/js-plugins/solid/src/index.ts b/js-plugins/solid/src/index.ts index 8679b2e53..2398aa3f0 100644 --- a/js-plugins/solid/src/index.ts +++ b/js-plugins/solid/src/index.ts @@ -44,7 +44,7 @@ export default function farmPluginSolid( return { name: 'farm-plugin-solid', - config(config, configEnv) { + config(config) { return { compilation: { lazyCompilation: @@ -55,23 +55,25 @@ export default function farmPluginSolid( } }; }, - configResolved(param) { + configResolved(config) { + const root = config.root ?? process.cwd(); + const mode = config.compilation?.mode; // We inject the dev mode only if the use˜r explicitly wants it or if we are in dev (serve) mode - needHmr = param.mode !== 'production'; - replaceDev = options.dev === true || param.mode === 'development'; - projectRoot = param.root ?? process.cwd(); + needHmr = mode !== 'production'; + replaceDev = options.dev === true || mode === 'development'; + projectRoot = root ?? process.cwd(); - if (!param.resolve) { - param.resolve = {}; + if (!config.compilation.resolve) { + config.compilation.resolve = {}; } - param.resolve.conditions = [ - ...(param.resolve.conditions ?? []), + config.compilation.resolve.conditions = [ + ...(config.compilation.resolve.conditions ?? []), 'solid', ...(replaceDev ? ['development'] : []) ]; - param.resolve.alias = { - ...(param.resolve.alias ?? {}), + config.compilation.resolve.alias = { + ...(config.compilation.resolve.alias ?? {}), 'solid-refresh': runtimePublicPath }; }, diff --git a/js-plugins/svgr/farm.config.mjs b/js-plugins/svgr/farm.config.mjs index b652dac25..d6152a7b7 100644 --- a/js-plugins/svgr/farm.config.mjs +++ b/js-plugins/svgr/farm.config.mjs @@ -1,43 +1,11 @@ -import { builtinModules } from 'module'; import farmDtsPlugin from '@farmfe/js-plugin-dts'; +import { createFarmJsPluginBuildConfig } from '../../configs/farm-js-plugin.base.config.mjs'; /** * @type {import('@farmfe/core').UserConfig} */ -export default { - compilation: { - input: { - index: './src/index.ts' - }, - output: { - path: 'build/' + (process.env.FARM_FORMAT || 'cjs'), - entryFilename: - '[entryName].' + (process.env.FARM_FORMAT === 'esm' ? 'js' : 'cjs'), - targetEnv: 'node', - format: process.env.FARM_FORMAT || 'cjs' - }, - external: [ - ...builtinModules.map((m) => `^${m}$`), - ...builtinModules.map((m) => `^node:${m}$`) - ], - partialBundling: { - enforceResources: [ - { - name: 'index.js', - test: ['.+'] - } - ] - }, - minify: false, - sourcemap: false, - presetEnv: false - }, - server: { - hmr: false - }, - plugins: [ - farmDtsPlugin({ - tsConfigPath: './tsconfig.build.json' - }) - ] -}; +export default createFarmJsPluginBuildConfig([ + farmDtsPlugin({ + tsConfigPath: './tsconfig.build.json' + }) +]); \ No newline at end of file diff --git a/js-plugins/svgr/package.json b/js-plugins/svgr/package.json index 748922641..273e86991 100644 --- a/js-plugins/svgr/package.json +++ b/js-plugins/svgr/package.json @@ -7,7 +7,7 @@ "type": "module", "exports": { ".": { - "import": "./build/esm/index.js", + "import": "./build/esm/index.mjs", "types": "./build/cjs/index.d.ts", "require": "./build/cjs/index.cjs", "default": "./build/cjs/index.cjs" diff --git a/js-plugins/vue/farm.config.mjs b/js-plugins/vue/farm.config.mjs index c75abb2f6..f6dabb9b3 100644 --- a/js-plugins/vue/farm.config.mjs +++ b/js-plugins/vue/farm.config.mjs @@ -1,6 +1,6 @@ import { builtinModules } from 'module'; import farmDtsPlugin from '@farmfe/js-plugin-dts'; -import path from 'path'; + /** * @type {import('@farmfe/core').UserConfig} */ diff --git a/js-plugins/vue/src/farm-vue-plugin.ts b/js-plugins/vue/src/farm-vue-plugin.ts index 8364c8dd5..45f71dbaa 100644 --- a/js-plugins/vue/src/farm-vue-plugin.ts +++ b/js-plugins/vue/src/farm-vue-plugin.ts @@ -27,9 +27,6 @@ import { } from './utils.js'; import { compileStyle } from '@vue/compiler-sfc'; -// apply style langs -type ApplyStyleLangs = ['less', 'sass', 'scss', 'stylus']; - const stylesCodeCache: StylesCodeCache = {}; const applyStyleLangs = ['less', 'sass', 'scss', 'stylus']; const cacheDescriptor: CacheDescriptor = {}; @@ -66,7 +63,7 @@ export default function farmVuePlugin( }; }, configResolved(config) { - farmConfig = config || {}; + farmConfig = config.compilation || {}; }, load: { filters: { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 7c29554c1..6500cc8a2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -49,6 +49,10 @@ cli const resolveOptions = resolveCommandOptions(options); const configPath = getConfigPath(options.config); + if (root && !path.isAbsolute(root)) { + root = path.resolve(process.cwd(), root); + } + const defaultOptions = { root, compilation: { diff --git a/packages/core/binding/binding.d.ts b/packages/core/binding/binding.d.ts index f0fcc31d0..22f9f0060 100644 --- a/packages/core/binding/binding.d.ts +++ b/packages/core/binding/binding.d.ts @@ -90,6 +90,7 @@ export class Compiler { update(paths: Array, callback: (...args: any[]) => any, sync: boolean): object addWatchFiles(root: string, paths: Array): void hasModule(resolvedPath: string): boolean + getParentFiles(resolvedPath: string): Array resources(): Record watchModules(): Array relativeModulePaths(): Array diff --git a/packages/core/binding/index.d.ts b/packages/core/binding/index.d.ts index 3cb87c4c2..e1053613d 100644 --- a/packages/core/binding/index.d.ts +++ b/packages/core/binding/index.d.ts @@ -119,10 +119,6 @@ export interface Config { targetEnv?: 'browser' | 'node'; format?: 'cjs' | 'esm'; }; - env?: Record; - envDir?: string; - envFiles?: string[]; - envPrefix?: string | string[]; resolve?: { extensions?: string[]; alias?: Record; @@ -142,7 +138,6 @@ export interface Config { swcHelpersPath?: string; namespace?: string; }; - configFilePath?: string; watch?: boolean | WatcherOptions; assets?: { include?: string[]; diff --git a/packages/core/src/compiler/index.ts b/packages/core/src/compiler/index.ts index ff6282a03..cfd9f49a3 100644 --- a/packages/core/src/compiler/index.ts +++ b/packages/core/src/compiler/index.ts @@ -20,7 +20,7 @@ export interface UpdateQueueItem { export class Compiler { private _bindingCompiler: BindingCompiler; private _updateQueue: UpdateQueueItem[] = []; - private _onUpdateFinishQueue: (() => void)[] = []; + private _onUpdateFinishQueue: (() => void | Promise)[] = []; public compiling = false; @@ -89,7 +89,9 @@ export class Compiler { await this.update(next.paths, true, true).then(next.resolve); } else { this.compiling = false; - this._onUpdateFinishQueue.forEach((cb) => cb()); + for (const cb of this._onUpdateFinishQueue) { + await cb(); + } // clear update finish queue this._onUpdateFinishQueue = []; } @@ -108,6 +110,10 @@ export class Compiler { return this._bindingCompiler.hasModule(resolvedPath); } + getParentFiles(idOrResolvedPath: string): string[] { + return this._bindingCompiler.getParentFiles(idOrResolvedPath); + } + resources(): Record { return this._bindingCompiler.resources(); } diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index ab006ec8a..867662d11 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -7,13 +7,12 @@ import { pathToFileURL } from 'node:url'; import merge from 'lodash.merge'; import { - convertPlugin, getSortedPlugins, handleVitePlugins, resolveAsyncPlugins, resolveConfigHook, resolveConfigResolvedHook, - rustPluginResolver + resolveFarmPlugins } from '../plugin/index.js'; import { bindingPath, Config } from '../../binding/index.js'; import { DevServer } from '../server/index.js'; @@ -23,10 +22,10 @@ import { __FARM_GLOBAL__ } from './_global.js'; import { bold, clearScreen, + DefaultLogger, green, isArray, isEmptyObject, - isObject, isWindows, Logger, normalizePath @@ -36,10 +35,8 @@ import { normalizeOutput } from './normalize-config/normalize-output.js'; import { traceDependencies } from '../utils/trace-dependencies.js'; import type { - ConfigEnv, FarmCLIOptions, NormalizedServerConfig, - ResolveConfigType, ResolvedUserConfig, UserConfig, UserHmrConfig, @@ -68,36 +65,47 @@ export function defineFarmConfig( export async function resolveConfig( inlineOptions: FarmCLIOptions, logger: Logger, - command: 'serve' | 'build', mode?: CompilationMode -): Promise { +): Promise { // Clear the console according to the cli command checkClearScreen(inlineOptions); - let userConfig: ResolvedUserConfig = {}; + const getDefaultConfig = async () => { + const mergedUserConfig = mergeInlineCliOptions({}, inlineOptions); + const resolvedUserConfig = await resolveMergedUserConfig( + mergedUserConfig, + undefined, + inlineOptions.mode ?? mode + ); + resolvedUserConfig.server = normalizeDevServerOptions({}, mode); + resolvedUserConfig.compilation = await normalizeUserCompilationConfig( + resolvedUserConfig, + logger, + mode + ); + resolvedUserConfig.root = resolvedUserConfig.compilation.root; + resolvedUserConfig.jsPlugins = []; + resolvedUserConfig.rustPlugins = []; + return resolvedUserConfig; + }; // configPath may be file or directory - const { configPath, root } = inlineOptions; - + const { configPath } = inlineOptions; + // if the config file can not found, just merge cli options and return default if (!configPath) { - return mergeUserConfig(userConfig, inlineOptions); + return getDefaultConfig(); } if (!path.isAbsolute(configPath)) { throw new Error('configPath must be an absolute path'); } - userConfig = await loadFileConfig(userConfig, inlineOptions, logger); + const loadedUserConfig = await loadConfigFile(configPath, logger); - if (!userConfig.root) { - userConfig.root = root || process.cwd(); + if (!loadedUserConfig) { + return getDefaultConfig(); } - userConfig.mode = userConfig.compilation?.mode || mode; - userConfig.isBuild = command === 'build'; - userConfig.command = command; - const configEnv: ConfigEnv = { - mode, - command - }; + + const { config: userConfig, configFilePath } = loadedUserConfig; const { jsPlugins, rustPlugins } = await resolveFarmPlugins(userConfig); @@ -109,7 +117,11 @@ export async function resolveConfig( const vitePlugins = userConfig?.vitePlugins ?? []; // run config and configResolved hook if (vitePlugins.length) { - vitePluginAdapters = await handleVitePlugins(vitePlugins, userConfig); + vitePluginAdapters = await handleVitePlugins( + vitePlugins, + userConfig, + logger + ); } const sortFarmJsPlugins = getSortedPlugins([ @@ -117,37 +129,40 @@ export async function resolveConfig( ...vitePluginAdapters ]); - // TODO vite plugin hook need sort by `order` in config hooks !!! not priority or enforce - // Start running config hook for all plugins - const config = await resolveConfigHook( - userConfig, - configEnv, - sortFarmJsPlugins + const config = await resolveConfigHook(userConfig, sortFarmJsPlugins); + + const mergedUserConfig = mergeInlineCliOptions(config, inlineOptions); + const resolvedUserConfig = await resolveMergedUserConfig( + mergedUserConfig, + configFilePath, + inlineOptions.mode ?? mode ); + // normalize server config first cause it may be used in normalizeUserCompilationConfig + resolvedUserConfig.server = normalizeDevServerOptions( + resolvedUserConfig.server, + mode + ); // check port availability: auto increment the port if a conflict occurs const targetWeb = !( - userConfig.compilation?.output?.targetEnv === 'node' || userConfig.isBuild + userConfig.compilation?.output?.targetEnv === 'node' || + mode === 'production' ); - targetWeb && (await DevServer.resolvePortConflict(userConfig, logger)); + targetWeb && + (await DevServer.resolvePortConflict(resolvedUserConfig.server, logger)); - const normalizedConfig = await normalizeUserCompilationConfig( - inlineOptions, - config, + resolvedUserConfig.compilation = await normalizeUserCompilationConfig( + resolvedUserConfig, logger, mode ); + resolvedUserConfig.root = resolvedUserConfig.compilation.root; + resolvedUserConfig.jsPlugins = sortFarmJsPlugins; + resolvedUserConfig.rustPlugins = rustPlugins; - await resolveConfigResolvedHook(normalizedConfig, sortFarmJsPlugins); // Fix: Await the Promise and pass the resolved value to the function. + await resolveConfigResolvedHook(resolvedUserConfig, sortFarmJsPlugins); // Fix: Await the Promise and pass the resolved value to the function. - return { - config, - normalizedConfig: { - ...normalizedConfig, - jsPlugins: sortFarmJsPlugins, - rustPlugins - } - }; + return resolvedUserConfig; } type ServerConfig = { @@ -160,13 +175,11 @@ type ServerConfig = { * @returns resolved config that parsed to rust compiler */ export async function normalizeUserCompilationConfig( - inlineConfig: (FarmCLIOptions & UserConfig) | null, userConfig: ResolvedUserConfig, logger: Logger, mode: CompilationMode = 'development' -): Promise { - const { compilation, root, server, envDir, envPrefix } = userConfig; - +): Promise { + const { compilation, root } = userConfig; // resolve root path const resolvedRootPath = normalizePath( root ? path.resolve(root) : process.cwd() @@ -202,20 +215,6 @@ export async function normalizeUserCompilationConfig( const isDevelopment = config.mode === 'development'; config.coreLibPath = bindingPath; - config.configFilePath = userConfig.configFilePath; - - const resolvedEnvPath = envDir ? envDir : resolvedRootPath; - - const [userEnv, existsEnvFiles] = loadEnv( - inlineConfig?.mode ?? mode, - resolvedEnvPath, - envPrefix - ); - - config.envFiles = [ - ...(Array.isArray(config.envFiles) ? config.envFiles : []), - ...existsEnvFiles - ]; config.external = [ ...module.builtinModules.map((m) => `^${m}$`), @@ -247,23 +246,17 @@ export async function normalizeUserCompilationConfig( } } - config.env = { - ...userEnv, - NODE_ENV: process.env.NODE_ENV || mode - }; - config.define = Object.assign( { // skip self define - ['FARM' + '_PROCESS_ENV']: config.env + ['FARM' + '_PROCESS_ENV']: userConfig.env }, - userConfig.define, config?.define, // for node target, we should not define process.env.NODE_ENV config.output?.targetEnv === 'node' ? {} - : Object.keys(config.env).reduce((env: any, key) => { - env[`process.env.${key}`] = config.env[key]; + : Object.keys(userConfig.env || {}).reduce((env: any, key) => { + env[`process.env.${key}`] = userConfig.env[key]; return env; }, {}) ); @@ -327,19 +320,16 @@ export async function normalizeUserCompilationConfig( setProcessEnv(config.mode); - // TODO resolve other server port - const normalizedDevServerConfig = normalizeDevServerOptions(server, mode); - config.server = normalizedDevServerConfig; if ( config.output.targetEnv !== 'node' && isArray(config.runtime.plugins) && - normalizedDevServerConfig.hmr && + userConfig.server.hmr && !config.runtime.plugins.includes(hmrClientPluginPath) ) { config.runtime.plugins.push(hmrClientPluginPath); - config.define.FARM_HMR_PORT = String(normalizedDevServerConfig.hmr.port); - config.define.FARM_HMR_HOST = normalizedDevServerConfig.hmr.host; - config.define.FARM_HMR_PATH = normalizedDevServerConfig.hmr.path; + config.define.FARM_HMR_PORT = String(userConfig.server.hmr.port); + config.define.FARM_HMR_HOST = userConfig.server.hmr.host; + config.define.FARM_HMR_PATH = userConfig.server.hmr.path; } if ( @@ -389,7 +379,7 @@ export async function normalizeUserCompilationConfig( } } - return { config }; + return config; } export const DEFAULT_HMR_OPTIONS: Required = { @@ -473,10 +463,9 @@ async function readConfigFile( const fileName = `farm.config.bundle-{${Date.now()}-${Math.random() .toString(16) .split('.') - .join('')}}.cjs`; + .join('')}}.mjs`; const normalizedConfig = await normalizeUserCompilationConfig( - null, { compilation: { input: { @@ -485,7 +474,7 @@ async function readConfigFile( output: { entryFilename: '[entryName]', path: outputPath, - format: 'cjs', + format: 'esm', targetEnv: 'node' }, external: ['!^(\\./|\\.\\./|[A-Za-z]:\\\\|/).*'], @@ -504,16 +493,13 @@ async function readConfigFile( presetEnv: false, lazyCompilation: false, persistentCache: false - }, - server: { - hmr: false } }, logger ); const compiler = new Compiler({ - ...normalizedConfig, + config: normalizedConfig, jsPlugins: [], rustPlugins: [] }); @@ -545,39 +531,6 @@ async function readConfigFile( } } -export function mergeUserConfig( - config: Record, - options: Record -) { - // The merge property can only be enabled if command line arguments are passed - return mergeConfiguration(config, options); -} - -export function mergeConfiguration( - a: Record, - b: Record -): Record { - const result: Record = { ...a }; - for (const key in b) { - if (Object.prototype.hasOwnProperty.call(b, key)) { - const value = b[key]; - if (value == null) { - continue; - } - if (isArray(value)) { - result[key] = result[key] - ? [...new Set([...result[key], ...value])] - : value; - } else if (isObject(value)) { - result[key] = mergeConfiguration(result[key] || {}, value); - } else { - result[key] = value; - } - } - } - return result; -} - export function normalizePublicDir(root: string, userPublicDir?: string) { const publicDir = userPublicDir ?? 'public'; const absPublicDirPath = path.isAbsolute(publicDir) @@ -650,83 +603,127 @@ function checkClearScreen(inlineConfig: FarmCLIOptions) { } } -async function loadFileConfig( - userConfig: ResolvedUserConfig, - inlineOptions: FarmCLIOptions, - logger: Logger -): Promise { - const { configPath } = inlineOptions; - // if configPath points to a directory, try to find a config file in it using default config - if (fs.statSync(configPath).isDirectory()) { - for (const name of DEFAULT_CONFIG_NAMES) { - const resolvedPath = path.join(configPath, name); - const config = await readConfigFile(resolvedPath, logger); - const farmConfig = mergeUserConfig(config, inlineOptions); - if (config) { - userConfig = parseUserConfig(farmConfig); - userConfig.configFilePath = resolvedPath; - // if we found a config file, stop searching - break; - } - } - } else if (fs.statSync(configPath).isFile()) { - const config = await readConfigFile(configPath, logger); - const farmConfig = mergeUserConfig(config, inlineOptions); +function mergeInlineCliOptions( + userConfig: UserConfig, + inlineOptions: FarmCLIOptions +): UserConfig { + if (inlineOptions.root) { + const cliRoot = inlineOptions.root; - if (config) { - userConfig = parseUserConfig(farmConfig); - userConfig.configFilePath = configPath; + if (!isAbsolute(cliRoot)) { + userConfig.root = path.resolve(process.cwd(), cliRoot); + } else { + userConfig.root = cliRoot; } } - if (userConfig.configFilePath) { - const dependencies = await traceDependencies(userConfig.configFilePath); - dependencies.sort(); - userConfig.configFileDependencies = dependencies; + // set compiler options + ['minify', 'sourcemap'].forEach((option: keyof FarmCLIOptions) => { + if (inlineOptions[option] !== undefined) { + userConfig.compilation = { + ...(userConfig.compilation ?? {}), + [option]: inlineOptions[option] + }; + } + }); + if (inlineOptions.outDir) { + userConfig.compilation = { + ...(userConfig.compilation ?? {}) + }; + userConfig.compilation.output = { + ...(userConfig.compilation.output ?? {}), + path: inlineOptions.outDir + }; } - delete userConfig.configPath; + + // set server options + ['port', 'open', 'https', 'hmr', 'host', 'strictPort'].forEach( + (option: keyof FarmCLIOptions) => { + if (inlineOptions[option] !== undefined) { + userConfig.server = { + ...(userConfig.server ?? {}), + [option]: inlineOptions[option] + }; + } + } + ); + return userConfig; } -async function resolveFarmPlugins(config: UserConfig) { - const plugins = config.plugins ?? []; +async function resolveMergedUserConfig( + mergedUserConfig: UserConfig, + configFilePath: string | undefined, + mode: 'development' | 'production' | string +) { + const resolvedUserConfig = { ...mergedUserConfig } as ResolvedUserConfig; + + // set internal config + resolvedUserConfig.envMode = mode; - if (!plugins.length) { - return { - rustPlugins: [], - jsPlugins: [] - }; + if (configFilePath) { + const dependencies = await traceDependencies(configFilePath); + dependencies.sort(); + resolvedUserConfig.configFileDependencies = dependencies; + resolvedUserConfig.configFilePath = configFilePath; } - const rustPlugins = []; + const resolvedRootPath = resolvedUserConfig.root ?? process.cwd(); + const resolvedEnvPath = resolvedUserConfig.envDir + ? resolvedUserConfig.envDir + : resolvedRootPath; - const jsPlugins: JsPlugin[] = []; + const [userEnv, existsEnvFiles] = loadEnv( + resolvedUserConfig.envMode ?? mode, + resolvedEnvPath, + resolvedUserConfig.envPrefix + ); - for (const plugin of plugins) { - if ( - typeof plugin === 'string' || - (isArray(plugin) && typeof plugin[0] === 'string') - ) { - rustPlugins.push(await rustPluginResolver(plugin as string, config.root)); - } else if (isObject(plugin)) { - convertPlugin(plugin as unknown as JsPlugin); - jsPlugins.push(plugin as unknown as JsPlugin); - } else if (isArray(plugin)) { - for (const pluginNestItem of plugin as JsPlugin[]) { - convertPlugin(pluginNestItem as JsPlugin); - jsPlugins.push(pluginNestItem as JsPlugin); + resolvedUserConfig.envFiles = [ + ...(Array.isArray(resolvedUserConfig.envFiles) + ? resolvedUserConfig.envFiles + : []), + ...existsEnvFiles + ]; + + resolvedUserConfig.env = { + ...userEnv, + NODE_ENV: process.env.NODE_ENV || mode + }; + + return resolvedUserConfig; +} + +/** + * Load config file from the specified path and return the config and config file path + * @param configPath the config path, could be a directory or a file + * @param logger custom logger + * @returns loaded config and config file path + */ +export async function loadConfigFile( + configPath: string, + logger: Logger = new DefaultLogger() +): Promise<{ config: UserConfig; configFilePath: string } | undefined> { + // if configPath points to a directory, try to find a config file in it using default config + if (fs.statSync(configPath).isDirectory()) { + for (const name of DEFAULT_CONFIG_NAMES) { + const resolvedPath = path.join(configPath, name); + const config = await readConfigFile(resolvedPath, logger); + + if (config) { + return { + config: parseUserConfig(config), + configFilePath: resolvedPath + }; } - } else { - throw new Error( - `plugin ${plugin} is not supported, Please pass the correct plugin type` - ); } + } else if (fs.statSync(configPath).isFile()) { + const config = await readConfigFile(configPath, logger); + return { + config: config && parseUserConfig(config), + configFilePath: configPath + }; } - - return { - rustPlugins, - jsPlugins - }; } function checkCompilationInputValue(userConfig: UserConfig, logger: Logger) { diff --git a/packages/core/src/config/normalize-config/normalize-persistent-cache.ts b/packages/core/src/config/normalize-config/normalize-persistent-cache.ts index ee22bd796..a656c55e3 100644 --- a/packages/core/src/config/normalize-config/normalize-persistent-cache.ts +++ b/packages/core/src/config/normalize-config/normalize-persistent-cache.ts @@ -12,7 +12,7 @@ export async function normalizePersistentCache( ) { if ( config?.persistentCache === false || - config.configFilePath === undefined + resolvedUserConfig.configFilePath === undefined ) { return; } @@ -21,12 +21,12 @@ export async function normalizePersistentCache( config.persistentCache = { buildDependencies: [], moduleCacheKeyStrategy: {}, - envs: config.env + envs: resolvedUserConfig.env }; } if (config.persistentCache.envs === undefined) { - config.persistentCache.envs = config.env; + config.persistentCache.envs = resolvedUserConfig.env; } if (!config.persistentCache.buildDependencies) { @@ -48,10 +48,10 @@ export async function normalizePersistentCache( } // trace all build dependencies of the config file - if (config.configFilePath) { + if (resolvedUserConfig.configFilePath) { const files = resolvedUserConfig?.configFileDependencies?.length ? resolvedUserConfig.configFileDependencies - : await traceDependencies(config.configFilePath); + : await traceDependencies(resolvedUserConfig.configFilePath); const packages = []; @@ -70,6 +70,7 @@ export async function normalizePersistentCache( packages.push(...(rustPlugins ?? [])); if (packages?.length) { + // console.log('packages', config); const require = createRequire(path.join(config.root, 'package.json')); for (const p of packages) { diff --git a/packages/core/src/config/types.ts b/packages/core/src/config/types.ts index e4942c821..faa5c704f 100644 --- a/packages/core/src/config/types.ts +++ b/packages/core/src/config/types.ts @@ -8,6 +8,7 @@ import type { ProxiesOptions } from '../server/middlewares/proxy.js'; import type { JsPlugin } from '../plugin/type.js'; import type { RustPlugin } from '../plugin/rust/index.js'; import type { Config } from '../../binding/index.js'; +import { Middleware } from 'koa'; export interface UserServerConfig { headers?: OutgoingHttpHeaders | undefined; @@ -34,16 +35,11 @@ export type NormalizedServerConfig = Required< } >; -export interface ConfigEnv { - command: 'build' | 'serve'; - mode: string; +export interface NormalizedConfig { + compilationConfig: Config; + serverConfig?: NormalizedServerConfig; } -export type ResolveConfigType = { - config?: UserConfig; - normalizedConfig?: Config; -}; - export interface UserHmrConfig { /** ignored watch paths of the module graph, entries of this option should be a string regexp */ ignores?: string[]; @@ -62,7 +58,7 @@ type InternalConfig = Config['config'] extends undefined type AvailableUserConfigKeys = Exclude< keyof InternalConfig, - 'configFilePath' | 'env' | 'envPrefix' | 'envFiles' | 'coreLibPath' | 'root' + 'configFilePath' | 'env' | 'coreLibPath' | 'root' >; export interface UserConfig { @@ -84,15 +80,17 @@ export interface UserConfig { } export interface ResolvedUserConfig extends UserConfig { - inlineConfig?: FarmCLIOptions; - configPath?: string; - isBuild?: boolean; - command?: 'serve' | 'build'; - mode?: 'development' | 'production'; + env?: Record; + envDir?: string; + envFiles?: string[]; + envPrefix?: string | string[]; configFilePath?: string; - define?: Record; - // TODO set this field for persistent cache + envMode?: string; configFileDependencies?: string[]; + compilation?: Config['config']; + server?: NormalizedServerConfig; + jsPlugins?: JsPlugin[]; + rustPlugins?: [string, string][]; } export interface GlobalFarmCLIOptions { @@ -136,4 +134,6 @@ export interface FarmCLIOptions clearScreen?: boolean; } -export type DevServerMiddleware = (context: DevServer) => void; +export type DevServerMiddleware = ( + context: DevServer +) => Middleware | undefined; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d1506c3d7..5d191e346 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,7 +15,6 @@ import fse from 'fs-extra'; import { Compiler } from './compiler/index.js'; import { - normalizeDevServerOptions, normalizePublicDir, resolveConfig, UserConfig @@ -23,13 +22,12 @@ import { import { DefaultLogger } from './utils/logger.js'; import { DevServer } from './server/index.js'; import { FileWatcher } from './watcher/index.js'; -import { Config } from '../binding/index.js'; import { compilerHandler } from './utils/build.js'; import { setProcessEnv } from './config/env.js'; import { bold, cyan, green, magenta } from './utils/color.js'; import { useProxy } from './server/middlewares/index.js'; -import type { FarmCLIOptions } from './config/types.js'; +import type { FarmCLIOptions, ResolvedUserConfig } from './config/types.js'; import { JsPlugin } from './plugin/type.js'; import { __FARM_GLOBAL__ } from './config/_global.js'; import { ConfigWatcher } from './watcher/configWatcher.js'; @@ -41,62 +39,69 @@ export async function start( setProcessEnv('development'); - const { config, normalizedConfig } = await resolveConfig( + const resolvedUserConfig = await resolveConfig( inlineConfig, logger, - 'serve', 'development' ); + const { + compilation: compilationConfig, + server: serverConfig, + jsPlugins, + rustPlugins + } = resolvedUserConfig; + const compiler = new Compiler({ + config: compilationConfig, + jsPlugins, + rustPlugins + }); - const compiler = new Compiler(normalizedConfig); - const devServer = new DevServer(compiler, logger, config, normalizedConfig); - devServer.createFarmServer(devServer.userConfig.server); - - if (normalizedConfig.config.mode === 'development') { - normalizedConfig.jsPlugins.forEach((plugin: JsPlugin) => - plugin.configDevServer?.(devServer) - ); + for (const plugin of jsPlugins) { + await plugin.configureCompiler?.(compiler); } + const devServer = new DevServer(compiler, logger); + devServer.createFarmServer(serverConfig); + + jsPlugins.forEach((plugin: JsPlugin) => + plugin.configureDevServer?.(devServer) + ); + await devServer.listen(); let fileWatcher: FileWatcher; // Make sure the server is listening before we watch for file changes if (devServer.config.hmr) { - if (normalizedConfig.config.mode === 'production') { + if (compilationConfig.mode === 'production') { logger.error( 'HMR can not be enabled in production mode. Please set the mode option to "development" in your config file.' ); process.exit(1); } - fileWatcher = new FileWatcher(devServer, { - ...normalizedConfig, - ...config - }); + fileWatcher = new FileWatcher(devServer, resolvedUserConfig); await fileWatcher.watch(); } - const farmWatcher = new ConfigWatcher({ - config: normalizedConfig, - userConfig: config - }).watch((filenames: string[]) => { - logger.info( - green( - `${filenames - .map((filename) => path.relative(config.root, filename)) - .join(', ')} changed, will restart server` - ) - ); + const farmWatcher = new ConfigWatcher(resolvedUserConfig).watch( + (filenames: string[]) => { + logger.info( + green( + `${filenames + .map((filename) => path.relative(resolvedUserConfig.root, filename)) + .join(', ')} changed, will restart server` + ) + ); - farmWatcher.close(); + farmWatcher.close(); - devServer.restart(async () => { - fileWatcher?.close(); - await devServer.closeFarmServer(); - __FARM_GLOBAL__.__FARM_RESTART_DEV_SERVER__ = true; - await start(inlineConfig); - }); - }); + devServer.restart(async () => { + fileWatcher?.close(); + await devServer.closeFarmServer(); + __FARM_GLOBAL__.__FARM_RESTART_DEV_SERVER__ = true; + await start(inlineConfig); + }); + } + ); } export async function build( @@ -104,44 +109,37 @@ export async function build( ): Promise { const logger = inlineConfig.logger ?? new DefaultLogger(); setProcessEnv('production'); - const { config, normalizedConfig } = await resolveConfig( + const resolvedUserConfig = await resolveConfig( inlineConfig, logger, - 'build', 'production' ); - setProcessEnv(normalizedConfig.config.mode); + setProcessEnv(resolvedUserConfig.compilation.mode); - await createBundleHandler(normalizedConfig, config); + await createBundleHandler(resolvedUserConfig); // copy resources under publicDir to output.path const absPublicDirPath = normalizePublicDir( - normalizedConfig.config.root, + resolvedUserConfig.root, inlineConfig.publicDir ); if (existsSync(absPublicDirPath)) { - fse.copySync(absPublicDirPath, normalizedConfig.config.output.path); + fse.copySync(absPublicDirPath, resolvedUserConfig.compilation.output.path); } } export async function preview(inlineConfig: FarmCLIOptions): Promise { const logger = inlineConfig.logger ?? new DefaultLogger(); const port = inlineConfig.port ?? 1911; - const { config, normalizedConfig } = await resolveConfig( + const resolvedUserConfig = await resolveConfig( inlineConfig, logger, - 'serve', 'production' ); - const normalizedDevServerConfig = normalizeDevServerOptions( - config.server, - 'production' - ); - - const { root, output } = normalizedConfig.config; + const { root, output } = resolvedUserConfig.compilation; const distDir = path.resolve(root, output.path); try { statSync(distDir); @@ -167,7 +165,7 @@ export async function preview(inlineConfig: FarmCLIOptions): Promise { const app = new Koa(); // support proxy - useProxy(normalizedDevServerConfig.proxy, app, logger); + useProxy(resolvedUserConfig.server.proxy, app, logger); app.use(compression()); app.use(async (ctx) => { @@ -214,41 +212,38 @@ export async function watch( ): Promise { const logger = inlineConfig.logger ?? new DefaultLogger(); setProcessEnv('development'); - const { config, normalizedConfig } = await resolveConfig( + const resolvedUserConfig = await resolveConfig( inlineConfig, logger, - 'build', 'development' ); - setProcessEnv(normalizedConfig.config.mode); + setProcessEnv(resolvedUserConfig.compilation.mode); const compilerFileWatcher = await createBundleHandler( - normalizedConfig, - config, + resolvedUserConfig, true ); - const farmWatcher = new ConfigWatcher({ - userConfig: config, - config: normalizedConfig - }).watch(async (files: string[]) => { - logger.info( - green( - `${files - .map((file) => path.relative(normalizedConfig.config.root, file)) - .join(', ')} changed, will be restart` - ) - ); + const farmWatcher = new ConfigWatcher(resolvedUserConfig).watch( + async (files: string[]) => { + logger.info( + green( + `${files + .map((file) => path.relative(resolvedUserConfig.root, file)) + .join(', ')} changed, will be restart` + ) + ); - farmWatcher.close(); + farmWatcher.close(); - __FARM_GLOBAL__.__FARM_RESTART_DEV_SERVER__ = true; + __FARM_GLOBAL__.__FARM_RESTART_DEV_SERVER__ = true; - compilerFileWatcher?.close(); + compilerFileWatcher?.close(); - await watch(inlineConfig); - }); + await watch(inlineConfig); + } + ); } export async function clean( @@ -307,56 +302,28 @@ async function findNodeModulesRecursively(rootPath: string): Promise { } export async function createBundleHandler( - normalizedConfig: Config, - userConfig: UserConfig, + resolvedUserConfig: ResolvedUserConfig, watchMode = false ) { - const compiler = new Compiler(normalizedConfig); - if (normalizedConfig.config.mode === 'production') { - normalizedConfig.jsPlugins.forEach((plugin: JsPlugin) => - plugin.configCompiler?.(compiler) - ); + const compiler = new Compiler({ + config: resolvedUserConfig.compilation, + jsPlugins: resolvedUserConfig.jsPlugins, + rustPlugins: resolvedUserConfig.rustPlugins + }); + + for (const plugin of resolvedUserConfig.jsPlugins) { + await plugin.configureCompiler?.(compiler); } + await compilerHandler(async () => { compiler.removeOutputPathDir(); await compiler.compile(); compiler.writeResourcesToDisk(); + }, resolvedUserConfig); - // const maxFileNameLength = Math.max( - // ...Object.keys(compiler.resources()).map((name) => name.length) - // ); - // const fileSizeMap = Object.entries(compiler.resources()) - // .filter(([name]) => !name.endsWith('.map')) - // .map(([resourceName, resource]) => { - // let c = green; - // const size = Buffer.byteLength(resource) / 1024; - - // if (size > 500) { - // c = yellow; - // } - - // const sizeStr = c(size.toFixed(0)) + cyan(' KB'); - - // return { - // resourceName: resourceName.padEnd(maxFileNameLength + 4, ' '), - // size: sizeStr - // }; - // }); - - // console.log(`\n${green('Output Files:')}`); - // fileSizeMap.forEach(({ resourceName, size }) => - // console.log(`\t${cyan(resourceName)}\t${size}`) - // ); - }, normalizedConfig); - - if (normalizedConfig.config?.watch || watchMode) { - const watcher = new FileWatcher(compiler, { - ...normalizedConfig, - ...userConfig - }); - + if (resolvedUserConfig.compilation?.watch || watchMode) { + const watcher = new FileWatcher(compiler, resolvedUserConfig); await watcher.watch(); - return watcher; } } diff --git a/packages/core/src/plugin/index.ts b/packages/core/src/plugin/index.ts index 160e9863d..6f400bb23 100644 --- a/packages/core/src/plugin/index.ts +++ b/packages/core/src/plugin/index.ts @@ -1,38 +1,24 @@ import { isArray, isObject } from '../utils/index.js'; -import { convertPlugin, handleVitePlugins } from './js/index.js'; +import { convertPlugin } from './js/index.js'; import { rustPluginResolver } from './rust/index.js'; import type { JsPlugin } from './type.js'; -import type { Config } from '../../binding/index.js'; -import { ConfigEnv, type UserConfig } from '../config/index.js'; +import { ResolvedUserConfig, type UserConfig } from '../config/index.js'; import merge from 'lodash.merge'; export * from './js/index.js'; export * from './rust/index.js'; -/** - * resolvePlugins split / jsPlugins / rustPlugins - * @param config - */ -export async function resolveAllPlugins( - finalConfig: Config['config'], - userConfig: UserConfig -) { - const plugins = userConfig.plugins ?? []; - const vitePlugins = (userConfig.vitePlugins ?? []).filter(Boolean); +export async function resolveFarmPlugins(config: UserConfig) { + const plugins = config.plugins ?? []; - if (!plugins.length && !vitePlugins?.length) { + if (!plugins.length) { return { rustPlugins: [], - jsPlugins: [], - finalConfig + jsPlugins: [] }; } - const vitePluginAdapters: JsPlugin[] = await handleVitePlugins( - vitePlugins, - userConfig - ); const rustPlugins = []; const jsPlugins: JsPlugin[] = []; @@ -43,7 +29,7 @@ export async function resolveAllPlugins( (isArray(plugin) && typeof plugin[0] === 'string') ) { rustPlugins.push( - await rustPluginResolver(plugin as string, finalConfig.root) + await rustPluginResolver(plugin as string, config.root ?? process.cwd()) ); } else if (isObject(plugin)) { convertPlugin(plugin as unknown as JsPlugin); @@ -59,18 +45,10 @@ export async function resolveAllPlugins( ); } } - // vite plugins execute after farm plugins by default. - jsPlugins.push(...vitePluginAdapters); - - // call user config hooks - for (const jsPlugin of jsPlugins) { - finalConfig = (await jsPlugin.config?.(finalConfig)) ?? finalConfig; - } return { rustPlugins, - jsPlugins, - finalConfig + jsPlugins }; } @@ -93,7 +71,6 @@ export async function resolveAsyncPlugins(arr: T[]): Promise { export async function resolveConfigHook( config: UserConfig, - configEnv: ConfigEnv, plugins: JsPlugin[] ): Promise { let conf = config; @@ -109,10 +86,8 @@ export async function resolveConfigHook( } for (const p of uniqueVitePlugins.values()) { - const hook = p.config; - - if (hook) { - const res = await p.config(conf, configEnv); + if (p.config) { + const res = await p.config(conf); if (res) { conf = merge(conf, res); @@ -124,15 +99,12 @@ export async function resolveConfigHook( } export async function resolveConfigResolvedHook( - config: Config, + config: ResolvedUserConfig, plugins: JsPlugin[] ) { - const conf = config; - for (const p of plugins) { - const hook = p.configResolved; - if (hook) { - await p.configResolved(conf.config); + if (p.configResolved) { + await p.configResolved(config); } } } diff --git a/packages/core/src/plugin/js/farm-to-vite-config.ts b/packages/core/src/plugin/js/farm-to-vite-config.ts index 6093f05b8..b3794cfc1 100644 --- a/packages/core/src/plugin/js/farm-to-vite-config.ts +++ b/packages/core/src/plugin/js/farm-to-vite-config.ts @@ -5,7 +5,7 @@ import { throwIncompatibleError } from './utils.js'; import merge from 'lodash.merge'; -import { Config } from '../../../binding/index.js'; +import { Logger } from '../../index.js'; export function farmUserConfigToViteConfig(config: UserConfig): ViteUserConfig { const vitePlugins = config.vitePlugins.map((plugin) => { @@ -61,65 +61,11 @@ export function farmUserConfigToViteConfig(config: UserConfig): ViteUserConfig { return viteConfig; } -export function farmNormalConfigToViteConfig( - config: Config['config'], - farmConfig: UserConfig -): ViteUserConfig { - const vitePlugins = farmConfig.vitePlugins.map((plugin) => { - if (typeof plugin === 'function') { - return plugin().vitePlugin; - } else { - return plugin; - } - }); - - return { - root: config.root, - base: config?.output?.publicPath ?? '/', - publicDir: farmConfig.publicDir ?? 'public', - mode: config?.mode, - define: config?.define, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ignore this error - command: config?.mode === 'production' ? 'build' : 'serve', - resolve: { - alias: config?.resolve?.alias, - extensions: config?.resolve?.extensions, - mainFields: config?.resolve?.mainFields, - conditions: config?.resolve?.conditions, - preserveSymlinks: config?.resolve?.symlinks === false - }, - plugins: vitePlugins, - server: { - hmr: Boolean(farmConfig.server?.hmr), - port: farmConfig.server?.port, - host: farmConfig.server?.host, - strictPort: farmConfig.server?.strictPort, - https: farmConfig.server?.https, - proxy: farmConfig.server?.proxy as any, - open: farmConfig.server?.open - // other options are not supported in farm - }, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ignore this error - isProduction: config.compilation?.mode === 'production', - css: { - devSourcemap: false - }, - build: { - outDir: config?.output?.path, - sourcemap: Boolean(config?.sourcemap), - minify: config?.minify, - cssMinify: config?.minify, - ssr: config?.output?.targetEnv === 'node' - // other options are not supported in farm - } - }; -} export function proxyViteConfig( viteConfig: ViteUserConfig, - pluginName: string + pluginName: string, + logger: Logger ): ViteUserConfig { return new Proxy(viteConfig, { get(target, key) { @@ -144,11 +90,14 @@ export function proxyViteConfig( 'isProduction', 'css', 'build', + 'logger', // these fields are always undefined in farm // they are only used for compatibility 'legacy', 'optimizeDeps', - 'ssr' + 'ssr', + 'logLevel', + 'experimental' ]; if (allowedKeys.includes(String(key))) { @@ -163,7 +112,7 @@ export function proxyViteConfig( 'dedupe' ]; - return new Proxy(target.resolve, { + return new Proxy(target.resolve || {}, { get(resolveTarget, resolveKey) { if (typeof resolveKey !== 'string') { return target[resolveKey as unknown as keyof typeof target]; @@ -201,7 +150,7 @@ export function proxyViteConfig( 'origin' ]; - return new Proxy(target.server, { + return new Proxy(target.server || {}, { get(serverTarget, serverKey) { if (typeof serverKey !== 'string') { return target[serverKey as unknown as keyof typeof target]; @@ -230,7 +179,7 @@ export function proxyViteConfig( } else if (key === 'css') { const allowedCssKeys = ['devSourcemap']; - return new Proxy(target.css, { + return new Proxy(target.css || {}, { get(cssTarget, cssKey) { if (typeof cssKey !== 'string') { return target[cssKey as unknown as keyof typeof target]; @@ -262,10 +211,11 @@ export function proxyViteConfig( 'sourcemap', 'minify', 'cssMinify', - 'ssr' + 'ssr', + 'watch' ]; - return new Proxy(target.build, { + return new Proxy(target.build || {}, { get(buildTarget, buildKey) { if (typeof buildKey !== 'string') { return target[buildKey as unknown as keyof typeof target]; @@ -290,6 +240,23 @@ export function proxyViteConfig( ); } }); + } else if (key === 'optimizeDeps') { + return new Proxy(target.optimizeDeps || {}, { + get(_, optimizeDepsKey) { + logger.warn( + `[vite-plugin] ${pluginName}: config "optimizeDeps" is not needed in farm, all of its options will be ignored. Current ignored option is: "${String( + optimizeDepsKey + )}"` + ); + + if (optimizeDepsKey === 'esbuildOptions') { + return {}; + } + return undefined; + } + }); + } else if (key === 'logger') { + return logger; } return target[key as keyof typeof target]; diff --git a/packages/core/src/plugin/js/farm-to-vite-context.ts b/packages/core/src/plugin/js/farm-to-vite-context.ts index 98cfe4c05..7ce697a25 100644 --- a/packages/core/src/plugin/js/farm-to-vite-context.ts +++ b/packages/core/src/plugin/js/farm-to-vite-context.ts @@ -114,7 +114,7 @@ export function farmContextToViteContext( }, meta: { rollupVersion: '3.29.4', - watchMode: config.compilation.mode !== 'production' + watchMode: config.compilation?.mode !== 'production' }, parse: (_) => { throw new Error( diff --git a/packages/core/src/plugin/js/index.ts b/packages/core/src/plugin/js/index.ts index 8dabcecec..54b76ea04 100644 --- a/packages/core/src/plugin/js/index.ts +++ b/packages/core/src/plugin/js/index.ts @@ -2,7 +2,8 @@ import merge from 'lodash.merge'; import { type JsPlugin, normalizeDevServerOptions, - type UserConfig + type UserConfig, + Logger } from '../../index.js'; import { DEFAULT_FILTERS, @@ -21,7 +22,8 @@ type VitePluginsType = VitePluginType[]; export async function handleVitePlugins( vitePlugins: VitePluginsType, - userConfig: UserConfig + userConfig: UserConfig, + logger: Logger ): Promise { const jsPlugins: JsPlugin[] = []; @@ -45,7 +47,8 @@ export async function handleVitePlugins( vitePlugin = plugin; filters = f; } - processVitePlugin(vitePlugin, userConfig, filters, jsPlugins); + + processVitePlugin(vitePlugin, userConfig, filters, jsPlugins, logger); } // if vitePlugins is not empty, append a load plugin to load file @@ -123,13 +126,15 @@ export function processVitePlugin( vitePlugin: VitePluginType, userConfig: UserConfig, filters: string[], - jsPlugins: JsPlugin[] + jsPlugins: JsPlugin[], + logger: Logger ) { const processPlugin = (plugin: any) => { const vitePluginAdapter = new VitePluginAdapter( plugin as any, userConfig, - filters + filters, + logger ); convertPlugin(vitePluginAdapter); jsPlugins.push(vitePluginAdapter); diff --git a/packages/core/src/plugin/js/vite-plugin-adapter.ts b/packages/core/src/plugin/js/vite-plugin-adapter.ts index d9afd3bb3..1122cb8f9 100644 --- a/packages/core/src/plugin/js/vite-plugin-adapter.ts +++ b/packages/core/src/plugin/js/vite-plugin-adapter.ts @@ -21,7 +21,7 @@ import { transformResourceInfo2RollupRenderedChunk, transformRollupResource2FarmResource } from './utils.js'; -import type { UserConfig } from '../../config/types.js'; +import type { ResolvedUserConfig, UserConfig } from '../../config/types.js'; import type { DevServer } from '../../server/index.js'; // only use types from vite and we do not install vite as a dependency @@ -57,7 +57,6 @@ import { } from './vite-server-adapter.js'; import { farmContextToViteContext } from './farm-to-vite-context.js'; import { - farmNormalConfigToViteConfig, farmUserConfigToViteConfig, proxyViteConfig, viteConfigToFarmConfig @@ -67,6 +66,7 @@ import { transformResourceInfo2RollupResource, transformFarmConfigToRollupNormalizedInputOptions } from './utils.js'; +import { Logger } from '../../index.js'; type OmitThis any> = T extends ( this: any, @@ -74,6 +74,9 @@ type OmitThis any> = T extends ( ) => infer R ? (...arg: A) => R : T; +type ObjectHook> = + | T + | ({ handler: T; order?: 'pre' | 'post' | null } & O); /// turn a vite plugin to farm js plugin export class VitePluginAdapter implements JsPlugin { @@ -84,6 +87,7 @@ export class VitePluginAdapter implements JsPlugin { private _farmConfig: UserConfig; private _viteConfig: ViteUserConfig; private _viteDevServer: ViteDevServerAdapter; + private _logger: Logger; buildStart: JsPlugin['buildStart']; resolve: JsPlugin['resolve']; @@ -96,13 +100,18 @@ export class VitePluginAdapter implements JsPlugin { renderStart: JsPlugin['renderStart']; augmentResourceHash?: JsPlugin['augmentResourceHash']; finalizeResources: JsPlugin['finalizeResources']; + writeResources: JsPlugin['writeResources']; // filter for js plugin to improve performance filters: string[]; - constructor(rawPlugin: Plugin, farmConfig: UserConfig, filters: string[]) { + constructor( + rawPlugin: Plugin, + farmConfig: UserConfig, + filters: string[], + logger: Logger + ) { this.name = rawPlugin.name; - if (!rawPlugin.name) { throw new Error( `Vite plugin ${rawPlugin} is not compatible with Farm for now. Because plugin name is required in Farm.` @@ -113,35 +122,45 @@ export class VitePluginAdapter implements JsPlugin { this._rawPlugin = rawPlugin; this._farmConfig = farmConfig; this._viteConfig = farmUserConfigToViteConfig(farmConfig); + this._logger = logger; this.filters = filters; + const hooksMap = { + buildStart: () => + (this.buildStart = this.viteBuildStartToFarmBuildStart()), + resolveId: () => (this.resolve = this.viteResolveIdToFarmResolve()), + load: () => (this.load = this.viteLoadToFarmLoad()), + transform: () => (this.transform = this.viteTransformToFarmTransform()), + buildEnd: () => (this.buildEnd = this.viteBuildEndToFarmBuildEnd()), + closeBundle: () => (this.finish = this.viteCloseBundleToFarmFinish()), + handleHotUpdate: () => + (this.updateModules = this.viteHandleHotUpdateToFarmUpdateModules()), + renderChunk: () => + (this.renderResourcePot = + this.viteHandleRenderChunkToFarmRenderResourcePot()), + renderStart: () => + (this.renderStart = this.viteRenderStartToFarmRenderStart()), + augmentChunkHash: () => + (this.augmentResourceHash = + this.viteAugmentChunkHashToFarmAugmentResourceHash()), + generateBundle: () => + (this.finalizeResources = + this.viteGenerateBundleToFarmFinalizeResources()), + writeBundle: () => + (this.writeResources = this.viteWriteBundleToFarmWriteResources()) + }; // convert hooks - if (rawPlugin.buildStart) - this.buildStart = this.viteBuildStartToFarmBuildStart(); - if (rawPlugin.buildStart) this.resolve = this.viteResolveIdToFarmResolve(); - if (rawPlugin.load) this.load = this.viteLoadToFarmLoad(); - if (rawPlugin.transform) - this.transform = this.viteTransformToFarmTransform(); - if (rawPlugin.buildEnd) this.buildEnd = this.viteBuildEndToFarmBuildEnd(); - if (rawPlugin.closeBundle) this.finish = this.viteCloseBundleToFarmFinish(); - if (rawPlugin.handleHotUpdate) - this.updateModules = this.viteHandleHotUpdateToFarmUpdateModules(); - if (rawPlugin.renderChunk) - this.renderResourcePot = - this.viteHandleRenderChunkToFarmRenderResourcePot(); - if (rawPlugin.renderStart) - this.renderStart = this.viteRenderStartToFarmRenderStart(); - if (rawPlugin.augmentChunkHash) - this.augmentResourceHash = - this.viteAugmentChunkHashToFarmAugmentResourceHash(); - if (rawPlugin.generateBundle) - this.finalizeResources = this.viteGenerateBundleToFarmFinalizeResources(); + for (const [hookName, fn] of Object.entries(hooksMap)) { + if (rawPlugin[hookName as keyof Plugin]) { + fn(); + } + } // if other unsupported vite plugins hooks are used, throw error const unsupportedHooks = [ 'transformIndexHtml', - 'writeBundle', + // 'writeBundle', 'renderError', 'resolveDynamicImport', 'resolveFileUrl', @@ -162,9 +181,10 @@ export class VitePluginAdapter implements JsPlugin { } // call both config and configResolved - async config(config: UserConfig, configEnv: ConfigEnv) { + async config(config: UserConfig) { this._farmConfig = config; this._viteConfig = farmUserConfigToViteConfig(this._farmConfig); + const configHook = this.wrapRawPluginHook('config', this._rawPlugin.config); if (configHook) { @@ -172,11 +192,12 @@ export class VitePluginAdapter implements JsPlugin { merge( this._viteConfig, await configHook( - proxyViteConfig(this._viteConfig, this.name), - configEnv + proxyViteConfig(this._viteConfig, this.name, this._logger), + this.getViteConfigEnv() ) ), - this.name + this.name, + this._logger ); this._farmConfig = viteConfigToFarmConfig( @@ -189,10 +210,14 @@ export class VitePluginAdapter implements JsPlugin { return this._farmConfig; } - async configResolved(config: Config['config']) { + async configResolved(config: ResolvedUserConfig) { if (!this._rawPlugin.configResolved) return; - - this._viteConfig = farmNormalConfigToViteConfig(config, this._farmConfig); + this._farmConfig = config; + this._viteConfig = proxyViteConfig( + farmUserConfigToViteConfig(config), + this.name, + this._logger + ); const configResolvedHook = this.wrapRawPluginHook( 'configResolved', @@ -204,7 +229,7 @@ export class VitePluginAdapter implements JsPlugin { } } - async configDevServer(devServer: DevServer) { + async configureDevServer(devServer: DevServer) { const hook = this.wrapRawPluginHook( 'configureServer', this._rawPlugin.configureServer @@ -212,7 +237,8 @@ export class VitePluginAdapter implements JsPlugin { this._viteDevServer = createViteDevServerAdapter( this.name, - this._viteConfig + this._viteConfig, + devServer ); if (hook) { @@ -225,14 +251,14 @@ export class VitePluginAdapter implements JsPlugin { } } - // private getViteConfigEnv(): ConfigEnv { - // return { - // ssrBuild: this._farmConfig.compilation?.output?.targetEnv === 'node', - // command: - // this._farmConfig.compilation?.mode === 'production' ? 'build' : 'serve', - // mode: this._farmConfig.compilation?.mode - // }; - // } + private getViteConfigEnv(): ConfigEnv { + return { + ssrBuild: this._farmConfig.compilation?.output?.targetEnv === 'node', + command: + this._farmConfig.compilation?.mode === 'production' ? 'build' : 'serve', + mode: this._farmConfig.compilation?.mode + }; + } private shouldExecutePlugin() { const command = @@ -261,18 +287,35 @@ export class VitePluginAdapter implements JsPlugin { private wrapRawPluginHook( hookName: string, - hook: object | undefined | ((...args: any[]) => any), + hook?: ObjectHook<(...args: any[]) => any, { sequential?: boolean }>, farmContext?: CompilationContext, currentHandlingFile?: string - ) { + ): ( + ...args: any[] + ) => any | undefined | Promise<(...args: any[]) => any | undefined> { if (hook === undefined) { return undefined; } - if (typeof hook !== 'function') { - throw new Error( - `${hookName} hook of vite plugin ${this.name} is not a function. Farm only supports vite plugin with function hooks. This Plugin is not compatible with farm.` - ); + if (typeof hook === 'object') { + if (!hook.handler) { + return undefined; + } + + const logWarn = (name: string) => { + this._logger.warn( + `Farm does not support '${name}' property of vite plugin ${this.name} hook ${hookName} for now. It will be ignored.` + ); + }; + // TODO support order, if a hook has order, it should be split into two plugins + if (hook.order) { + logWarn('order'); + } + if (hook.sequential) { + logWarn('sequential'); + } + + hook = hook.handler; } if (farmContext) { @@ -338,6 +381,7 @@ export class VitePluginAdapter implements JsPlugin { } return path.concat(''); }; + if (isString(resolveIdResult)) { return { resolvedPath: removeQuery(encodeStr(resolveIdResult)), @@ -365,7 +409,6 @@ export class VitePluginAdapter implements JsPlugin { private viteLoadToFarmLoad(): JsPlugin['load'] { return { filters: { - // TODO support internal filter optimization for common plugins like @vitejs/plugin-vue resolvedPaths: this.filters }, executor: this.wrapExecutor( @@ -581,7 +624,7 @@ export class VitePluginAdapter implements JsPlugin { context ) as OmitThis; - const hash = await hook( + const hash = await hook?.( transformResourceInfo2RollupRenderedChunk(param) ); @@ -608,7 +651,7 @@ export class VitePluginAdapter implements JsPlugin { {} as OutputBundle ); - hook( + await hook?.( transformFarmConfigToRollupNormalizedOutputOptions(param.config), bundles ); @@ -625,6 +668,33 @@ export class VitePluginAdapter implements JsPlugin { }; } + private viteWriteBundleToFarmWriteResources(): JsPlugin['writeResources'] { + return { + executor: this.wrapExecutor( + async (param: FinalizeResourcesHookParams, context) => { + const hook = this.wrapRawPluginHook( + 'writeBundle', + this._rawPlugin.writeBundle, + context + ); + + const bundles = Object.entries(param.resourcesMap).reduce( + (res, [key, val]) => { + res[key] = transformResourceInfo2RollupResource(val); + return res; + }, + {} as OutputBundle + ); + + await hook?.( + transformFarmConfigToRollupNormalizedOutputOptions(param.config), + bundles + ); + } + ) + }; + } + // skip farm lazy compilation virtual module for vite plugin public static isFarmInternalVirtualModule(id: string) { return ( diff --git a/packages/core/src/plugin/js/vite-server-adapter.ts b/packages/core/src/plugin/js/vite-server-adapter.ts index eb014b776..9b76dd0e0 100644 --- a/packages/core/src/plugin/js/vite-server-adapter.ts +++ b/packages/core/src/plugin/js/vite-server-adapter.ts @@ -1,4 +1,6 @@ // import { watch } from 'chokidar'; +import { DevServer } from '../../index.js'; +import WsServer from '../../server/ws.js'; import { CompilationContext } from '../type.js'; import { throwIncompatibleError } from './utils.js'; @@ -9,8 +11,9 @@ export class ViteDevServerAdapter { watcher: any; middlewares: any; middlewareCallbacks: any[]; + ws: WsServer; - constructor(pluginName: string, config: any) { + constructor(pluginName: string, config: any, server: DevServer) { this.moduleGraph = createViteModuleGraphAdapter(pluginName); this.config = config; this.pluginName = pluginName; @@ -50,6 +53,8 @@ export class ViteDevServerAdapter { } } ); + + this.ws = server.ws; } } @@ -93,23 +98,31 @@ export class ViteModuleGraphAdapter { } } -export function createViteDevServerAdapter(pluginName: string, config: any) { - const proxy = new Proxy(new ViteDevServerAdapter(pluginName, config), { - get(target, key) { - const allowedKeys = [ - 'moduleGraph', - 'config', - 'watcher', - 'middlewares', - 'middlewareCallbacks' - ]; - if (allowedKeys.includes(String(key))) { - return target[key as keyof typeof target]; - } +export function createViteDevServerAdapter( + pluginName: string, + config: any, + server: DevServer +) { + const proxy = new Proxy( + new ViteDevServerAdapter(pluginName, config, server), + { + get(target, key) { + const allowedKeys = [ + 'moduleGraph', + 'config', + 'watcher', + 'middlewares', + 'middlewareCallbacks', + 'ws' + ]; + if (allowedKeys.includes(String(key))) { + return target[key as keyof typeof target]; + } - throwIncompatibleError(pluginName, 'viteDevServer', allowedKeys, key); + throwIncompatibleError(pluginName, 'viteDevServer', allowedKeys, key); + } } - }); + ); return proxy; } diff --git a/packages/core/src/plugin/type.ts b/packages/core/src/plugin/type.ts index 24896334c..1e76f8a73 100644 --- a/packages/core/src/plugin/type.ts +++ b/packages/core/src/plugin/type.ts @@ -7,7 +7,12 @@ import { PluginTransformHookParam, PluginTransformHookResult } from '../../binding/index.js'; -import { Compiler, ConfigEnv, DevServer, UserConfig } from '../index.js'; +import { + Compiler, + DevServer, + ResolvedUserConfig, + UserConfig +} from '../index.js'; export interface CompilationContextEmitFileParams { resolvedPath: string; @@ -120,31 +125,27 @@ type JsPluginHook = { filters: F; executor: Callback }; export interface JsPlugin { name: string; priority?: number; - apply?: - | 'serve' - | 'build' - | ((this: void, config: UserConfig, env: ConfigEnv) => boolean); + // apply?: + // | 'serve' + // | 'build' + // | ((this: void, config: UserConfig, env: ConfigEnv) => boolean); // config?: Callback; - config?: ( - config: UserConfig, - configEnv?: ConfigEnv - ) => UserConfig | Promise; + config?: (config: UserConfig) => UserConfig | Promise; - configResolved?: (config: Config['config']) => void; + configResolved?: (config: ResolvedUserConfig) => void | Promise; /** * runs in development mode only * @param server * @returns */ - configDevServer?: (server: DevServer) => void; + configureDevServer?: (server: DevServer) => void | Promise; /** - * runs in production mode only - * @param server + * @param compiler * @returns */ - configCompiler?: (compiler: Compiler) => void; + configureCompiler?: (compiler: Compiler) => void | Promise; buildStart?: { executor: Callback, void> }; @@ -199,6 +200,10 @@ export interface JsPlugin { >; }; + writeResources?: { + executor: Callback>; + }; + pluginCacheLoaded?: { executor: Callback; }; diff --git a/packages/core/src/server/hmr-engine.ts b/packages/core/src/server/hmr-engine.ts index a8a36227c..386e9b0af 100644 --- a/packages/core/src/server/hmr-engine.ts +++ b/packages/core/src/server/hmr-engine.ts @@ -45,7 +45,6 @@ export class HmrEngine { return; } - this._updateQueue = []; let updatedFilesStr = queue .map((item) => { if (isAbsolute(item)) { @@ -65,6 +64,7 @@ export class HmrEngine { } const start = Date.now(); + const result = await this._compiler.update(queue); this._logger.info( `${cyan(updatedFilesStr)} updated in ${bold( @@ -72,13 +72,10 @@ export class HmrEngine { )}` ); - // TODO: write resources to disk when hmr finished in incremental mode - // if (this._devServer.config?.writeToDisk) { - // this._compiler.onUpdateFinish(() => { - // this._compiler.writeResourcesToDisk(); - // console.log('writeResourcesToDisk'); - // }); - // } + // clear update queue after update finished + this._updateQueue = this._updateQueue.filter( + (item) => !queue.includes(item) + ); let dynamicResourcesMap: Record = null; @@ -112,60 +109,38 @@ export class HmrEngine { this.callUpdates(result); - // const id = Date.now().toString(); - // // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // // @ts-ignore TODO fix this - // this._updateResults.set(id, { - // result: resultStr, - // count: this._devServer.ws.clients.size - // }); this._devServer.ws.clients.forEach((client: WebSocketClient) => { - client.send(resultStr); + client.rawSend(` + { + type: 'farm-update', + result: ${resultStr} + } + `); }); - // if there are more updates, recompile again - if (this._updateQueue.length > 0) { - await this.recompileAndSendResult(); - } + this._compiler.onUpdateFinish(async () => { + // if there are more updates, recompile again + if (this._updateQueue.length > 0) { + await this.recompileAndSendResult(); + } + }); }; - async hmrUpdate(path: string) { - // if lazy compilation is enabled, we need to update the virtual module - if (this._compiler.config.config.lazyCompilation) { - if (this._compiler.hasModule(path) && !this._updateQueue.includes(path)) { - this._updateQueue.push(path); - } + async hmrUpdate(absPath: string | string[]) { + const paths = Array.isArray(absPath) ? absPath : [absPath]; - if (!this._compiler.compiling) { - await this.recompileAndSendResult(); - } - } else if (this._compiler.hasModule(path)) { - if (!this._updateQueue.includes(path)) { + for (const path of paths) { + if (this._compiler.hasModule(path) && !this._updateQueue.includes(path)) { this._updateQueue.push(path); } + } - if (!this._compiler.compiling) { + if (!this._compiler.compiling) { + try { await this.recompileAndSendResult(); + } catch (e) { + this._logger.error(e); } } } - - // getUpdateResult(id: string) { - // const result = this._updateResults.get(id); - - // if (result) { - // result.count--; - - // // there are no more clients waiting for this update - // if (result.count <= 0 && this._updateResults.size >= 2) { - // /** - // * Edge handle - // * The BrowserExtension the user's browser may replay the request, resulting in an error that the result.id cannot be found. - // * So keep the result of the last time every time, so that the request can be successfully carried out. - // */ - // this._updateResults.delete(this._updateResults.keys().next().value); - // } - // } - // return result?.result; - // } } diff --git a/packages/core/src/server/index.ts b/packages/core/src/server/index.ts index c83203e34..26227293f 100644 --- a/packages/core/src/server/index.ts +++ b/packages/core/src/server/index.ts @@ -6,11 +6,9 @@ import { Compiler } from '../compiler/index.js'; import { DEFAULT_HMR_OPTIONS, DevServerMiddleware, - normalizeDevServerOptions, NormalizedServerConfig, normalizePublicDir, normalizePublicPath, - UserConfig, UserServerConfig } from '../config/index.js'; import { HmrEngine } from './hmr-engine.js'; @@ -22,18 +20,16 @@ import { printServerUrls } from '../utils/index.js'; import { - corsPlugin, - headersPlugin, - hmrPlugin, - lazyCompilationPlugin, - proxyPlugin, - recordsPlugin, - resourcesPlugin + cors, + headers, + lazyCompilation, + proxy, + records, + resources } from './middlewares/index.js'; import { __FARM_GLOBAL__ } from '../config/_global.js'; import { resolveServerUrls } from '../utils/http.js'; import WsServer from './ws.js'; -import { Config } from '../../binding/index.js'; import { Server } from './type.js'; /** @@ -82,32 +78,20 @@ export class DevServer implements ImplDevServer { server?: Server; publicDir?: string; publicPath?: string; - userConfig?: UserConfig; - compilationConfig?: Config; - - constructor( - private _compiler: Compiler, - public logger: Logger, - options?: UserConfig, - compilationConfig?: Config - ) { - this.publicDir = normalizePublicDir( - _compiler.config.config.root, - options.publicDir - ); + + constructor(private compiler: Compiler, public logger: Logger) { + this.publicDir = normalizePublicDir(compiler.config.config.root); this.publicPath = normalizePublicPath( - options?.compilation?.output?.publicPath, + compiler.config.config.output?.publicPath, logger, false ) || '/'; - this.compilationConfig = compilationConfig; - this.userConfig = options; } getCompiler(): Compiler { - return this._compiler; + return this.compiler; } app(): Koa { @@ -125,7 +109,7 @@ export class DevServer implements ImplDevServer { // compile the project and start the dev server await this.compile(); - bootstrap(Date.now() - start, this.compilationConfig); + bootstrap(Date.now() - start, this.compiler.config); await this.startServer(this.config); @@ -141,11 +125,11 @@ export class DevServer implements ImplDevServer { } private async compile(): Promise { - await this._compiler.compile(); + await this.compiler.compile(); if (this.config.writeToDisk) { const base = this.publicPath.match(/^https?:\/\//) ? '' : this.publicPath; - this._compiler.writeResourcesToDisk(base); + this.compiler.writeResourcesToDisk(base); } } @@ -207,7 +191,7 @@ export class DevServer implements ImplDevServer { await Promise.all(promises); } - public createFarmServer(options: UserServerConfig) { + public createFarmServer(options: NormalizedServerConfig) { const { https, host = 'localhost', middlewares = [] } = options; const protocol = https ? 'https' : 'http'; let hostname; @@ -216,10 +200,11 @@ export class DevServer implements ImplDevServer { } else { hostname = 'localhost'; } - this.config = normalizeDevServerOptions( - { ...options, protocol, hostname }, - 'development' - ); + this.config = { + ...options, + protocol, + hostname + }; this._app = new Koa(); if (https) { @@ -234,13 +219,23 @@ export class DevServer implements ImplDevServer { this.server = http.createServer(this._app.callback()); } - this.ws = new WsServer(this.server, this.config, true); + this.hmrEngine = new HmrEngine(this.compiler, this, this.logger); + this.ws = new WsServer(this.server, this.config, this.hmrEngine); + + // Note: path should be Farm's id, which is a relative path in dev mode, + // but in vite, it's a url path like /xxx/xxx.js + this.ws.on('vite:invalidate', ({ path, message }) => { + // find hmr boundary starting from the parent of the file + this.logger.info(`HMR invalidate: ${path}. ${message ?? ''}`); + const parentFiles = this.compiler.getParentFiles(path); + this.hmrEngine.hmrUpdate(parentFiles); + }); this._context = { config: this.config, app: this._app, server: this.server, - compiler: this._compiler, + compiler: this.compiler, logger: this.logger, serverOptions: {} }; @@ -248,15 +243,9 @@ export class DevServer implements ImplDevServer { } static async resolvePortConflict( - userConfig: UserConfig, + normalizedDevConfig: NormalizedServerConfig, logger: Logger ): Promise { - const normalizedDevConfig = normalizeDevServerOptions( - userConfig.server, - 'development' - ); - userConfig.server = normalizedDevConfig; - let devPort = normalizedDevConfig.port; let hmrPort = DEFAULT_HMR_OPTIONS.port; const { strictPort, host } = normalizedDevConfig; @@ -289,8 +278,8 @@ export class DevServer implements ImplDevServer { let isPortAvailableResult = await isPortAvailable(devPort); while (isPortAvailableResult === false) { - userConfig.server.hmr = { port: ++hmrPort }; - userConfig.server.port = ++devPort; + normalizedDevConfig.hmr.port = ++hmrPort; + normalizedDevConfig.port = ++devPort; isPortAvailableResult = await isPortAvailable(devPort); } } @@ -311,25 +300,36 @@ export class DevServer implements ImplDevServer { private resolvedFarmServerMiddleware( middlewares?: DevServerMiddleware[] ): void { - const resolvedPlugins = [ + const internalMiddlewares = [ ...(middlewares || []), - headersPlugin, - lazyCompilationPlugin, - hmrPlugin, - corsPlugin, - resourcesPlugin, - recordsPlugin, - proxyPlugin + headers, + lazyCompilation, + cors, + resources, + records, + proxy ]; - resolvedPlugins.forEach((plugin) => plugin(this)); + internalMiddlewares.forEach((middleware) => { + const middlewareImpl = middleware(this); + + if (middlewareImpl) { + if (Array.isArray(middlewareImpl)) { + middlewareImpl.forEach((m) => { + this._app.use(m); + }); + } else { + this._app.use(middlewareImpl); + } + } + }); } private async printServerUrls() { this._context.serverOptions.resolvedUrls = await resolveServerUrls( this.server, this.config, - this.userConfig + this.compiler.config.config.output?.publicPath ); if (this._context.serverOptions.resolvedUrls) { printServerUrls(this._context.serverOptions.resolvedUrls, this.logger); diff --git a/packages/core/src/server/middlewares/cors.ts b/packages/core/src/server/middlewares/cors.ts index 9c197faac..b8fafed8b 100644 --- a/packages/core/src/server/middlewares/cors.ts +++ b/packages/core/src/server/middlewares/cors.ts @@ -1,8 +1,9 @@ +import { Middleware } from 'koa'; import { DevServer } from '../index.js'; -import cors from '@koa/cors'; +import { default as koaCors } from '@koa/cors'; -export function corsPlugin(devSeverContext: DevServer) { - const { app, config } = devSeverContext._context; +export function cors(devSeverContext: DevServer): Middleware { + const { config } = devSeverContext._context; if (!config.cors) return; - app.use(cors(typeof config.cors === 'boolean' ? {} : config.cors)); + return koaCors(typeof config.cors === 'boolean' ? {} : config.cors); } diff --git a/packages/core/src/server/middlewares/headers.ts b/packages/core/src/server/middlewares/headers.ts index a0445d2a3..c560c543b 100644 --- a/packages/core/src/server/middlewares/headers.ts +++ b/packages/core/src/server/middlewares/headers.ts @@ -1,14 +1,16 @@ +import { Middleware } from 'koa'; import { DevServer } from '../index.js'; -export function headersPlugin(devSeverContext: DevServer) { - const { app, config } = devSeverContext._context; +export function headers(devSeverContext: DevServer): Middleware { + const { config } = devSeverContext._context; if (!config.headers) return; - app.use(async (ctx, next) => { + + return async (ctx, next) => { if (config.headers) { for (const name in config.headers) { ctx.set(name, config.headers[name] as string | string[]); } } await next(); - }); + }; } diff --git a/packages/core/src/server/middlewares/hmr.ts b/packages/core/src/server/middlewares/hmr.ts deleted file mode 100644 index 1fb800551..000000000 --- a/packages/core/src/server/middlewares/hmr.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * HMR middleware waits for HMR request from client and return the corresponding updated modules. - * - * When a file changed, the dev server will first update the modified file and its dependencies and send signals to the browser client via websocket, - * and store the updated result with a unique id in a cache, the client will send a `/__hmr?id=xxx` import() request to fetch the updated modules and execute it. - */ - -import { HmrEngine } from '../hmr-engine.js'; -import { DevServer } from '../index.js'; - -// /** -// * @deprecated HMR result is now served by websocket, send to client via websocket and get the result by `eval` the websocket message. -// * @param context DevServer -// */ - -export function hmrPlugin(devSeverContext: DevServer) { - const { config, _context, logger } = devSeverContext; - if (config.hmr) { - // TODO use diff hmr port with dev server - devSeverContext.hmrEngine = new HmrEngine( - _context.compiler, - devSeverContext, - logger - ); - } -} diff --git a/packages/core/src/server/middlewares/index.ts b/packages/core/src/server/middlewares/index.ts index 40164a469..8b58d8f59 100644 --- a/packages/core/src/server/middlewares/index.ts +++ b/packages/core/src/server/middlewares/index.ts @@ -1,6 +1,5 @@ export * from './cors.js'; export * from './headers.js'; -export * from './hmr.js'; export * from './lazy-compilation.js'; export * from './proxy.js'; export * from './records.js'; diff --git a/packages/core/src/server/middlewares/lazy-compilation.ts b/packages/core/src/server/middlewares/lazy-compilation.ts index 698a2f9f1..c55026b21 100644 --- a/packages/core/src/server/middlewares/lazy-compilation.ts +++ b/packages/core/src/server/middlewares/lazy-compilation.ts @@ -3,16 +3,20 @@ */ import { relative } from 'node:path'; -import { Context } from 'koa'; +import { Context, Middleware } from 'koa'; import { DevServer } from '../index.js'; import { bold, cyan, green } from '../../index.js'; import type { Resource } from '@farmfe/runtime/src/resource-loader.js'; -export function lazyCompilation(devSeverContext: DevServer) { +export function lazyCompilation(devSeverContext: DevServer): Middleware { const compiler = devSeverContext.getCompiler(); + if (!compiler.config.config?.lazyCompilation) { + return; + } + return async (ctx: Context, next: () => Promise) => { if (ctx.path === '/__lazy_compile') { const paths = (ctx.query.paths as string).split(','); @@ -69,9 +73,3 @@ export function lazyCompilation(devSeverContext: DevServer) { } }; } - -export function lazyCompilationPlugin(distance: DevServer) { - if (distance._context.compiler.config.config.lazyCompilation) { - distance._context.app.use(lazyCompilation(distance)); - } -} diff --git a/packages/core/src/server/middlewares/proxy.ts b/packages/core/src/server/middlewares/proxy.ts index f4e52172d..ad06f9232 100644 --- a/packages/core/src/server/middlewares/proxy.ts +++ b/packages/core/src/server/middlewares/proxy.ts @@ -1,7 +1,11 @@ -import proxy, { IKoaProxiesOptions, IBaseKoaProxiesOptions } from 'koa-proxies'; +import { + default as koaProxy, + IKoaProxiesOptions, + IBaseKoaProxiesOptions +} from 'koa-proxies'; import type { DevServer } from '../index.js'; import { UserConfig } from '../../config/types.js'; -import Application from 'koa'; +import Application, { Middleware } from 'koa'; import { Logger } from '../../utils/logger.js'; export type ProxiesOptions = IKoaProxiesOptions; @@ -26,7 +30,7 @@ export function useProxy( try { if (path.length > 0) { - app.use(proxy(path[0] === '^' ? new RegExp(path) : path, opts)); + app.use(koaProxy(path[0] === '^' ? new RegExp(path) : path, opts)); } } catch (err) { logger.error(`Error setting proxy for path ${path}: ${err.message}`); @@ -34,7 +38,7 @@ export function useProxy( } } -export function proxyPlugin(devSeverContext: DevServer) { +export function proxy(devSeverContext: DevServer): Middleware { const { app, config, logger } = devSeverContext._context; if (!config.proxy) { return; diff --git a/packages/core/src/server/middlewares/records.ts b/packages/core/src/server/middlewares/records.ts index 84f4b3451..21a8c6d70 100644 --- a/packages/core/src/server/middlewares/records.ts +++ b/packages/core/src/server/middlewares/records.ts @@ -2,13 +2,11 @@ * record middleware. */ -import { Context } from 'koa'; - +import { Context, Middleware } from 'koa'; import { DevServer } from '../index.js'; -export function records(devSeverContext: DevServer) { - const compiler = devSeverContext.getCompiler(); - +export function records(devServer: DevServer): Middleware { + const compiler = devServer.getCompiler(); return async (ctx: Context, next: () => Promise) => { if (ctx.path === '/__record/modules') { ctx.body = compiler.modules(); @@ -38,7 +36,3 @@ export function records(devSeverContext: DevServer) { } }; } - -export function recordsPlugin(distance: DevServer) { - distance._context.app.use(records(distance)); -} diff --git a/packages/core/src/server/middlewares/resources.ts b/packages/core/src/server/middlewares/resources.ts index 7e5c6f504..977922ece 100644 --- a/packages/core/src/server/middlewares/resources.ts +++ b/packages/core/src/server/middlewares/resources.ts @@ -3,7 +3,7 @@ */ import path, { extname } from 'node:path'; -import { Context, Next } from 'koa'; +import { Context, Middleware, Next } from 'koa'; import { Compiler } from '../../compiler/index.js'; import { DevServer } from '../index.js'; import koaStatic from 'koa-static'; @@ -12,7 +12,7 @@ import { generateFileTree, generateFileTreeHtml } from '../../utils/index.js'; import { existsSync, readFileSync, statSync } from 'node:fs'; // import mime from 'mime-types'; -export function resources( +export function resourcesMiddleware( compiler: Compiler, config: NormalizedServerConfig, publicPath: string @@ -110,22 +110,26 @@ export function resources( }; } -export function resourcesPlugin(devSeverContext: DevServer) { +export function resources( + devSeverContext: DevServer +): Middleware | Middleware[] { + const middlewares = []; if (!devSeverContext.config.writeToDisk) { - devSeverContext._context.app.use( - resources( + middlewares.push( + resourcesMiddleware( devSeverContext._context.compiler, devSeverContext.config, devSeverContext.publicPath ) ); } else { - devSeverContext._context.app.use( + middlewares.push( koaStatic(devSeverContext.getCompiler().config.config.output.path, { extensions: ['html'] }) ); } - devSeverContext._context.app.use(koaStatic(devSeverContext.publicDir)); + middlewares.push(koaStatic(devSeverContext.publicDir)); + return middlewares; } diff --git a/packages/core/src/server/ws.ts b/packages/core/src/server/ws.ts index dc5e794c1..583716792 100644 --- a/packages/core/src/server/ws.ts +++ b/packages/core/src/server/ws.ts @@ -10,6 +10,7 @@ import { red } from '../index.js'; import { Server } from './type.js'; +import { HmrEngine } from './hmr-engine.js'; const HMR_HEADER = 'farm_hmr'; @@ -37,6 +38,7 @@ export type WebSocketCustomListener = ( export interface WebSocketClient { send(payload: any): void; send(event: string, payload?: any['data']): void; + rawSend(str: string): void; socket: WebSocketRawType; } @@ -49,7 +51,7 @@ export default class WsServer implements IWebSocketServer { constructor( private httpServer: Server, private config: NormalizedServerConfig, - public isFarmHmrEvent: boolean = false, + private hmrEngine: HmrEngine, logger?: Logger ) { this.logger = logger ?? new DefaultLogger(); @@ -59,6 +61,7 @@ export default class WsServer implements IWebSocketServer { private createWebSocketServer() { try { this.wss = new WebSocketServerRaw({ noServer: true }); + this.connection(); // TODO IF not have httpServer this.httpServer.on('upgrade', this.upgradeWsServer.bind(this)); } catch (err) { @@ -138,7 +141,7 @@ export default class WsServer implements IWebSocketServer { this.send({ type: 'custom', event, data: payload }); } - public on(event: string, listener: () => void) { + public on(event: string, listener: (...args: any[]) => void) { if (wsServerEvents.includes(event)) { this.wss.on(event, listener); } else { @@ -164,16 +167,25 @@ export default class WsServer implements IWebSocketServer { } catch { this.logger.error('Failed to parse WebSocket message: ' + raw); } + // transform vite js-update to farm update + if (parsed?.type === 'js-update' && parsed?.path) { + this.hmrEngine.hmrUpdate(parsed.path); + return; + } + if (!parsed || parsed.type !== 'custom' || !parsed.event) return; const listeners = this.customListeners.get(parsed.event); if (!listeners?.size) return; const client = this.getSocketClient(socket); listeners.forEach((listener) => listener(parsed.data, client)); }); + socket.on('error', (err: Error & { code: string }) => { return this.handleSocketError(err); }); + socket.send(JSON.stringify({ type: 'connected' })); + if (this.bufferedError) { socket.send(JSON.stringify(this.bufferedError)); this.bufferedError = null; @@ -227,21 +239,17 @@ export default class WsServer implements IWebSocketServer { private getSocketClient(socket: WebSocketRawType) { if (!this.clientsMap.has(socket)) { this.clientsMap.set(socket, { - send: (...args) => - this.sendMessage(socket, this.isFarmHmrEvent, ...args), - socket + send: (...args) => this.sendMessage(socket, ...args), + socket, + rawSend: (str) => socket.send(str) }); } return this.clientsMap.get(socket); } - private sendMessage( - socket: WebSocketRawType, - isFarmHmrEvent: boolean, - ...args: any[] - ) { + private sendMessage(socket: WebSocketRawType, ...args: any[]) { let payload: any; - if (typeof args[0] === 'string' && !isFarmHmrEvent) { + if (typeof args[0] === 'string') { payload = { type: 'custom', event: args[0], @@ -250,7 +258,7 @@ export default class WsServer implements IWebSocketServer { } else { payload = args[0]; } - socket.send(payload); + socket.send(JSON.stringify(payload)); } private handleSocketError(err: Error & { code: string }) { diff --git a/packages/core/src/utils/build.ts b/packages/core/src/utils/build.ts index 4858605db..9982e69a5 100644 --- a/packages/core/src/utils/build.ts +++ b/packages/core/src/utils/build.ts @@ -1,14 +1,14 @@ import { performance } from 'node:perf_hooks'; import { DefaultLogger } from './logger.js'; -import type { Config } from '../../binding/index.js'; import { PersistentCacheBrand, bold, green } from './color.js'; +import { ResolvedUserConfig } from '../index.js'; export async function compilerHandler( callback: () => Promise, - config: Config + config: ResolvedUserConfig ) { - const usePersistentCache = config.config.persistentCache; + const usePersistentCache = config.compilation.persistentCache; const persistentCacheText = usePersistentCache ? bold(PersistentCacheBrand) : ''; @@ -27,7 +27,7 @@ export async function compilerHandler( `Build completed in ${bold( green(`${elapsedTime}ms`) )} ${persistentCacheText} Resources emitted to ${bold( - green(config.config.output.path) + green(config.compilation.output.path) )}.` ); } diff --git a/packages/core/src/utils/http.ts b/packages/core/src/utils/http.ts index d52aabf36..6733af5cb 100644 --- a/packages/core/src/utils/http.ts +++ b/packages/core/src/utils/http.ts @@ -30,7 +30,7 @@ export const wildcardHosts = new Set([ export async function resolveServerUrls( server: Server, options: UserServerConfig, - config: any + publicPath?: string ): Promise { const address = server.address(); const isAddressInfo = (x: any): x is AddressInfo => x?.address; @@ -44,7 +44,7 @@ export async function resolveServerUrls( const hostname = await resolveHostname(options.host); const protocol = options.https ? 'https' : 'http'; const { port } = getAddressHostnamePort(address); - const base = config.compilation?.output?.publicPath || ''; + const base = publicPath || ''; if (hostname.host !== undefined && !wildcardHosts.has(hostname.host)) { const url = createServerUrl(protocol, hostname.name, port, base); diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts index 8b2c6d141..f548f778f 100644 --- a/packages/core/src/utils/logger.ts +++ b/packages/core/src/utils/logger.ts @@ -3,11 +3,7 @@ import { fileURLToPath } from 'node:url'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { - ColorFunction, - PersistentCacheBrand, - colors -} from './color.js'; +import { ColorFunction, PersistentCacheBrand, colors } from './color.js'; import { Config } from '../../binding/index.js'; type LogLevelNames = 'trace' | 'debug' | 'info' | 'warn' | 'error'; @@ -84,9 +80,7 @@ export class DefaultLogger implements Logger { : 'log'; if (this.levelValues[level] <= this.levelValues[level]) { const prefix = showBanner ? this.prefix + ' ' : ''; - const loggerMessage = color - ? color(prefix + message) - : prefix + message; + const loggerMessage = color ? color(prefix + message) : prefix + message; console[loggerMethod](loggerMessage); } } @@ -98,7 +92,6 @@ export class DefaultLogger implements Logger { } } - trace(message: string): void { this.brandPrefix(colors.green); this.logMessage(LogLevel.Trace, message, colors.magenta); @@ -109,7 +102,6 @@ export class DefaultLogger implements Logger { this.logMessage(LogLevel.Debug, message, colors.blue); } - info(message: string, iOptions?: LoggerOptions): void { const options: LoggerOptions | undefined = iOptions; if (options) { @@ -131,7 +123,7 @@ export class DefaultLogger implements Logger { this.logMessage(LogLevel.Error, message, colors.red); const effectiveOptions = { ...this.options, ...errorOptions }; - + if (effectiveOptions.exit) { process.exit(1); } @@ -158,10 +150,14 @@ export class DefaultLogger implements Logger { export function printServerUrls(urls: any, logger: Logger): void { const colorUrl = (url: string) => - colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)); + colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)); const logUrl = (url: string, type: string) => - logger.info(`${colors.bold(colors.magenta('>'))} ${colors.bold(type)}${colors.bold(colorUrl(url))}`); + logger.info( + `${colors.bold(colors.magenta('>'))} ${colors.bold(type)}${colors.bold( + colorUrl(url) + )}` + ); urls.local.map((url: string) => logUrl(url, 'Local: ')); urls.network.map((url: string) => logUrl(url, 'Network: ')); @@ -182,11 +178,14 @@ export function bootstrap(times: number, config: Config) { 'utf-8' ) ).version; - console.log('\n', colors.bold(colors.brandColor(`${'ϟ'} Farm v${version}`))); console.log( - `${colors.bold(colors.green(` ✓`))} ${colors.bold('Ready in')} ${colors.bold( - colors.green(`${times}ms`) - )} ${persistentCacheFlag}`, + '\n', + colors.bold(colors.brandColor(`${'ϟ'} Farm v${version}`)) + ); + console.log( + `${colors.bold(colors.green(` ✓`))} ${colors.bold( + 'Ready in' + )} ${colors.bold(colors.green(`${times}ms`))} ${persistentCacheFlag}`, '\n' ); } diff --git a/packages/core/src/watcher/configWatcher.ts b/packages/core/src/watcher/configWatcher.ts index b2fba3c0a..e18981d6d 100644 --- a/packages/core/src/watcher/configWatcher.ts +++ b/packages/core/src/watcher/configWatcher.ts @@ -1,18 +1,15 @@ -import { Config, JsFileWatcher } from '../../binding/index.js'; +import { JsFileWatcher } from '../../binding/index.js'; import { ResolvedUserConfig } from '../config/index.js'; -interface WatcherOptions { - config: Config; - userConfig: ResolvedUserConfig; -} - export class ConfigWatcher { private watcher: JsFileWatcher; private _close = false; - constructor(private options: WatcherOptions) { - if (!options) { - throw new Error('Invalid options provided to Farm JsConfigWatcher'); + constructor(private resolvedUserConfig: ResolvedUserConfig) { + if (!resolvedUserConfig) { + throw new Error( + 'Invalid resolvedUserConfig provided to Farm JsConfigWatcher' + ); } } @@ -22,9 +19,11 @@ export class ConfigWatcher { } const watchedFilesSet = new Set([ - ...(this.options.config?.config.envFiles ?? []), - ...(this.options.userConfig?.configFileDependencies ?? []), - this.options.userConfig?.configFilePath + ...(this.resolvedUserConfig.envFiles ?? []), + ...(this.resolvedUserConfig.configFileDependencies ?? []), + ...(this.resolvedUserConfig.configFilePath + ? [this.resolvedUserConfig.configFilePath] + : []) ]); const watchedFiles = Array.from(watchedFilesSet).filter(Boolean); diff --git a/packages/core/src/watcher/index.ts b/packages/core/src/watcher/index.ts index 31c1ce80f..104a0aa1e 100644 --- a/packages/core/src/watcher/index.ts +++ b/packages/core/src/watcher/index.ts @@ -4,7 +4,7 @@ import debounce from 'lodash.debounce'; import { Compiler } from '../compiler/index.js'; import { DevServer } from '../server/index.js'; -import { Config, JsFileWatcher } from '../../binding/index.js'; +import { JsFileWatcher } from '../../binding/index.js'; import { compilerHandler, DefaultLogger } from '../utils/index.js'; import { DEFAULT_HMR_OPTIONS } from '../index.js'; @@ -23,9 +23,9 @@ export class FileWatcher implements ImplFileWatcher { constructor( public serverOrCompiler: DevServer | Compiler, - public options?: Config & ResolvedUserConfig + public options: ResolvedUserConfig ) { - this._root = options.config.root; + this._root = options.root; this._awaitWriteFinish = DEFAULT_HMR_OPTIONS.watchOptions.awaitWriteFinish; if (serverOrCompiler instanceof DevServer) { diff --git a/packages/core/tests/binding.spec.ts b/packages/core/tests/binding.spec.ts index a59e1fcb4..196075cd4 100644 --- a/packages/core/tests/binding.spec.ts +++ b/packages/core/tests/binding.spec.ts @@ -4,21 +4,23 @@ import { test } from 'vitest'; import { Compiler, DefaultLogger, + normalizeDevServerOptions, normalizeUserCompilationConfig } from '../src/index.js'; // just make sure the binding works test('Binding - should parse config to rust correctly', async () => { const currentDir = path.dirname(fileURLToPath(import.meta.url)); + const serverConfig = normalizeDevServerOptions({}, 'production'); const compilationConfig = await normalizeUserCompilationConfig( - null, { - root: path.resolve(currentDir, 'fixtures', 'binding') + root: path.resolve(currentDir, 'fixtures', 'binding'), + server: serverConfig }, new DefaultLogger() ); const compiler = new Compiler({ - ...compilationConfig, + config: compilationConfig, jsPlugins: [], rustPlugins: [] }); diff --git a/packages/core/tests/common.ts b/packages/core/tests/common.ts index 97e2540f5..4b6b7d3fb 100644 --- a/packages/core/tests/common.ts +++ b/packages/core/tests/common.ts @@ -12,7 +12,6 @@ export async function getCompiler( input?: Record ): Promise { const compilationConfig = await normalizeUserCompilationConfig( - null, { root, compilation: { @@ -28,9 +27,6 @@ export async function getCompiler( sourcemap: false, persistentCache: false }, - server: { - hmr: false - }, plugins }, new DefaultLogger(), @@ -38,7 +34,7 @@ export async function getCompiler( ); return new Compiler({ - config: compilationConfig.config, + config: compilationConfig, jsPlugins: plugins, rustPlugins: [] }); diff --git a/packages/core/tests/config.spec.ts b/packages/core/tests/config.spec.ts index 729e57431..cd1007cce 100644 --- a/packages/core/tests/config.spec.ts +++ b/packages/core/tests/config.spec.ts @@ -14,33 +14,50 @@ import { DefaultLogger } from '../src/utils/logger.js'; test('resolveUserConfig', async () => { const filePath = fileURLToPath(path.dirname(import.meta.url)); - const { config } = await resolveConfig( + const config = await resolveConfig( { configPath: path.join(filePath, 'fixtures', 'config', 'farm.config.ts') }, new DefaultLogger(), - 'serve', 'development' ); - expect(config).toEqual({ - compilation: { - input: { - main: './main.tsx' - }, - external: builtinModules + expect(config.compilation.define).toEqual({ + FARM_HMR_HOST: true, + FARM_HMR_PATH: '/__hmr', + FARM_HMR_PORT: '9000', + FARM_PROCESS_ENV: { + NODE_ENV: 'test' }, - isBuild: false, - command: 'serve', - root: process.cwd(), - mode: 'development', - configFilePath: path.join(filePath, 'fixtures', 'config', 'farm.config.ts'), - configFileDependencies: [ + 'process.env.NODE_ENV': 'test' + }); + expect(config.compilation.input).toEqual({ + main: './main.tsx' + }); + expect(config.compilation.output).toEqual({ + path: './dist', + publicPath: '/' + }); + expect(config.compilation.lazyCompilation).toEqual(true); + expect(config.compilation.sourcemap).toEqual(true); + expect(config.compilation.minify).toEqual(false); + expect(config.compilation.presetEnv).toEqual(false); + expect(config.compilation.persistentCache).toEqual({ + buildDependencies: [ // path.join(filePath, '..', 'src', 'config.ts'), path.join(filePath, 'fixtures', 'config', 'farm.config.ts'), path.join(filePath, 'fixtures', 'config', 'util.ts'), - 'module' + 'module', + 'package-lock.json', + 'pnpm-lock.yaml', + 'yarn.lock' ], - server: normalizeDevServerOptions(config.server, 'development') + envs: { + NODE_ENV: 'test' + }, + moduleCacheKeyStrategy: {} }); + expect(config.server).toEqual( + normalizeDevServerOptions(config.server, 'development') + ); }); describe('normalize-dev-server-options', () => { diff --git a/packages/core/types/LICENSE b/packages/core/types/LICENSE new file mode 100644 index 000000000..379ec3058 --- /dev/null +++ b/packages/core/types/LICENSE @@ -0,0 +1,49 @@ +# Farm core types license +Farm core types is released under the MIT license: + +MIT License + +Copyright (c) 2023-present, Brightwu(吴明亮) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +# Vite core license +Vite is released under the MIT license: + +MIT License + +Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/core/types/README.md b/packages/core/types/README.md new file mode 100644 index 000000000..226142727 --- /dev/null +++ b/packages/core/types/README.md @@ -0,0 +1,5 @@ +# Note + +Because Farm is compatible with Vite. Farm core types is the same as Vite's client types. see [License](./license). + +You can just use `import.meta.hot`, `import.meta.glob` the same as Vite. diff --git a/packages/core/types/custom-event.d.ts b/packages/core/types/custom-event.d.ts new file mode 100644 index 000000000..f8862a69a --- /dev/null +++ b/packages/core/types/custom-event.d.ts @@ -0,0 +1,35 @@ +import type { + ErrorPayload, + FullReloadPayload, + PrunePayload, + UpdatePayload +} from './hmr-payload'; + +export interface CustomEventMap { + 'vite:beforeUpdate': UpdatePayload; + 'vite:afterUpdate': UpdatePayload; + 'vite:beforePrune': PrunePayload; + 'vite:beforeFullReload': FullReloadPayload; + 'vite:error': ErrorPayload; + 'vite:invalidate': InvalidatePayload; + 'vite:ws:connect': WebSocketConnectionPayload; + 'vite:ws:disconnect': WebSocketConnectionPayload; +} + +export interface WebSocketConnectionPayload { + /** + * @experimental + * We expose this instance experimentally to see potential usage. + * This might be removed in the future if we didn't find reasonable use cases. + * If you find this useful, please open an issue with details so we can discuss and make it stable API. + */ + webSocket: WebSocket; +} + +export interface InvalidatePayload { + path: string; + message: string | undefined; +} + +export type InferCustomEventPayload = + T extends keyof CustomEventMap ? CustomEventMap[T] : any; diff --git a/packages/core/types/hmr-payload.d.ts b/packages/core/types/hmr-payload.d.ts new file mode 100644 index 000000000..13ccead3b --- /dev/null +++ b/packages/core/types/hmr-payload.d.ts @@ -0,0 +1,61 @@ +export type HMRPayload = + | ConnectedPayload + | UpdatePayload + | FullReloadPayload + | CustomPayload + | ErrorPayload + | PrunePayload; + +export interface ConnectedPayload { + type: 'connected'; +} + +export interface UpdatePayload { + type: 'update'; + updates: Update[]; +} + +export interface Update { + type: 'js-update' | 'css-update'; + path: string; + acceptedPath: string; + timestamp: number; + /** + * @experimental internal + */ + explicitImportRequired?: boolean | undefined; +} + +export interface PrunePayload { + type: 'prune'; + paths: string[]; +} + +export interface FullReloadPayload { + type: 'full-reload'; + path?: string; +} + +export interface CustomPayload { + type: 'custom'; + event: string; + data?: any; +} + +export interface ErrorPayload { + type: 'error'; + err: { + [name: string]: any; + message: string; + stack: string; + id?: string; + frame?: string; + plugin?: string; + pluginCode?: string; + loc?: { + file?: string; + line: number; + column: number; + }; + }; +} diff --git a/packages/core/types/hot.d.ts b/packages/core/types/hot.d.ts new file mode 100644 index 000000000..8ee40cabc --- /dev/null +++ b/packages/core/types/hot.d.ts @@ -0,0 +1,36 @@ +import type { InferCustomEventPayload } from './custom-event'; + +export type ModuleNamespace = Record & { + [Symbol.toStringTag]: 'Module'; +}; + +export interface ViteHotContext { + readonly data: any; + + accept(): void; + accept(cb: (mod: ModuleNamespace | undefined) => void): void; + accept(dep: string, cb: (mod: ModuleNamespace | undefined) => void): void; + accept( + deps: readonly string[], + cb: (mods: Array) => void + ): void; + + acceptExports( + exportNames: string | readonly string[], + cb?: (mod: ModuleNamespace | undefined) => void + ): void; + + dispose(cb: (data: any) => void): void; + prune(cb: (data: any) => void): void; + invalidate(message?: string): void; + + on( + event: T, + cb: (payload: InferCustomEventPayload) => void + ): void; + off( + event: T, + cb: (payload: InferCustomEventPayload) => void + ): void; + send(event: T, data?: InferCustomEventPayload): void; +} diff --git a/packages/core/types/import-meta.d.ts b/packages/core/types/import-meta.d.ts index 5fef2d854..1fb54f163 100644 --- a/packages/core/types/import-meta.d.ts +++ b/packages/core/types/import-meta.d.ts @@ -1,3 +1,18 @@ +interface ImportMetaEnv { + [key: string]: any; + BASE_URL: string; + MODE: string; + DEV: boolean; + PROD: boolean; + SSR: boolean; +} + interface ImportMeta { + url: string; + + readonly hot?: import('./hot').ViteHotContext; + + readonly env: ImportMetaEnv; + glob: import('./import-glob').ImportGlobFunction; } diff --git a/packages/runtime-plugin-hmr/LICENSE b/packages/runtime-plugin-hmr/LICENSE new file mode 100644 index 000000000..64826e851 --- /dev/null +++ b/packages/runtime-plugin-hmr/LICENSE @@ -0,0 +1,72 @@ +# Farm HMR license +Farm core types is released under the MIT license: + +MIT License + +Copyright (c) 2023-present, Brightwu(吴明亮) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +# esm-hmr license +MIT License + +Copyright (c) 2020 Fred K. Schott + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +# Vite core license +Vite is released under the MIT license: + +MIT License + +Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/runtime-plugin-hmr/README.md b/packages/runtime-plugin-hmr/README.md new file mode 100644 index 000000000..9876ff2ca --- /dev/null +++ b/packages/runtime-plugin-hmr/README.md @@ -0,0 +1,6 @@ +# Note + +Farm is compatible with Vite. The Farm HMR client's API and implementation is similar to Vite's. see [License](./license). + +- Farm's HMR client is compatible with Vite, see https://vitejs.dev/guide/api-hmr.html. +- And it's inspired by both Vite and esm-hmr, see https://github.com/FredKSchott/esm-hmr diff --git a/packages/runtime-plugin-hmr/src/hmr-client.ts b/packages/runtime-plugin-hmr/src/hmr-client.ts new file mode 100644 index 000000000..c405c18e1 --- /dev/null +++ b/packages/runtime-plugin-hmr/src/hmr-client.ts @@ -0,0 +1,226 @@ +import type { ModuleSystem } from '@farmfe/runtime'; +import { HMRPayload, HmrUpdateResult, RawHmrUpdateResult } from './types'; +import { HotModuleState } from './hot-module-state'; +import { logger } from './logger'; + +// Inject during compile time +const port = Number(FARM_HMR_PORT || 9000); +// TODO use import.meta to get hostname +const host = + typeof FARM_HMR_HOST === 'boolean' + ? window.location.hostname || 'localhost' + : FARM_HMR_HOST || 'localhost'; + +const path = FARM_HMR_PATH || '/__hmr'; + +export class HmrClient { + socket: WebSocket; + registeredHotModulesMap = new Map(); + disposeMap = new Map void | Promise>(); + pruneMap = new Map void | Promise>(); + customListenersMap = new Map< + string, + ((data: any) => void | Promise)[] + >(); + + constructor(private moduleSystem: ModuleSystem) {} + + connect() { + logger.log('connecting to the server...'); + + // setup websocket connection + const socket = new WebSocket(`ws://${host}:${port}${path}`, 'farm_hmr'); + this.socket = socket; + // listen for the message from the server + // when the user save the file, the server will recompile the file(and its dependencies as long as its dependencies are changed) + // after the file is recompiled, the server will generated a update resource and send its id to the client + // the client will apply the update + socket.addEventListener('message', (event) => { + const result: HMRPayload = eval(`(${event.data})`); + this.handleMessage(result); + }); + + socket.addEventListener( + 'open', + () => { + this.notifyListeners('vite:ws:connect', { webSocket: socket }); + }, + { once: true } + ); + + socket.addEventListener('close', () => { + this.notifyListeners('vite:ws:disconnect', { webSocket: socket }); + logger.log('disconnected from the server, please reload the page.'); + }); + + return socket; + } + + async applyHotUpdates(result: HmrUpdateResult, moduleSystem: ModuleSystem) { + result.changed.forEach((id) => { + logger.log(`${id} updated`); + }); + + for (const id of result.removed) { + const prune = this.pruneMap.get(id); + if (prune) { + const hotContext = this.registeredHotModulesMap.get(id); + await prune(hotContext.data); + } + + moduleSystem.delete(id); + this.registeredHotModulesMap.delete(id); + } + + for (const id of result.added) { + moduleSystem.register(id, result.modules[id]); + } + + for (const id of result.changed) { + moduleSystem.update(id, result.modules[id]); + + if (!result.boundaries[id]) { + // do not found boundary module, reload the window + window.location.reload(); + } + } + + if (result.dynamicResourcesMap) { + moduleSystem.dynamicModuleResourcesMap = result.dynamicResourcesMap; + } + + for (const chains of Object.values(result.boundaries)) { + for (const chain of chains) { + // clear the cache of the boundary module and its dependencies + for (const id of chain) { + moduleSystem.clearCache(id); + } + + try { + // require the boundary module + const boundary = chain[chain.length - 1]; + const hotContext = this.registeredHotModulesMap.get(boundary); + const acceptedDep = + chain.length > 1 ? chain[chain.length - 2] : undefined; + + if (!hotContext) { + console.error('hot context is empty for ', boundary); + window.location.reload(); + } + + // get all the accept callbacks of the boundary module that accepts the updated module + const selfAcceptedCallbacks = hotContext.acceptCallbacks.filter( + ({ deps }) => deps.includes(boundary) + ); + const depsAcceptedCallbacks = hotContext.acceptCallbacks.filter( + ({ deps }) => deps.includes(acceptedDep) + ); + // when there are both self accept callbacks and deps accept callbacks in a boundary module, only the deps accept callbacks will be called + for (const [acceptedId, acceptedCallbacks] of Object.entries({ + [acceptedDep]: depsAcceptedCallbacks, + [boundary]: selfAcceptedCallbacks + })) { + if (acceptedCallbacks.length > 0) { + const acceptHotContext = + this.registeredHotModulesMap.get(acceptedId); + + const disposer = this.disposeMap.get(acceptedId); + if (disposer) await disposer(acceptHotContext.data); + // clear accept callbacks, it will be re-registered in the accepted module when the module is required + acceptHotContext.acceptCallbacks = []; + + const acceptedExports = moduleSystem.require(acceptedId); + + for (const { deps, fn } of acceptedCallbacks) { + fn( + deps.map((dep) => + dep === acceptedId ? acceptedExports : undefined + ) + ); + } + // break the loop, only the first accept callback will be called + break; + } + } + } catch (err) { + // The boundary module's dependencies may not present in current module system for a multi-page application. We should reload the window in this case. + // See https://github.com/farm-fe/farm/issues/383 + console.error(err); + window.location.reload(); + } + } + } + } + + async notifyListeners(event: string, data: any) { + const callbacks = this.customListenersMap.get(event); + + if (callbacks) { + await Promise.allSettled(callbacks.map((cb) => cb(data))); + } + } + + /** + * handle vite HMR message, except farm-update which is handled by handleFarmUpdate, other messages are handled the same as vite + * @param payload Vite HMR payload + */ + async handleMessage(payload: HMRPayload) { + switch (payload.type) { + case 'farm-update': + this.notifyListeners('farm:beforeUpdate', payload); + this.handleFarmUpdate(payload.result); + this.notifyListeners('farm:afterUpdate', payload); + break; + case 'connected': + logger.log('connected to the server'); + break; + case 'update': + this.notifyListeners('vite:beforeUpdate', payload); + await Promise.all( + payload.updates.map(async (update) => { + if (update.type === 'js-update') { + this.socket.send(JSON.stringify(update)); + return; + } + + logger.warn('css link update is not supported yet'); + }) + ); + this.notifyListeners('vite:afterUpdate', payload); + break; + case 'custom': + this.notifyListeners(payload.event, payload.data); + break; + case 'full-reload': + this.notifyListeners('vite:beforeFullReload', payload); + window.location.reload(); + break; + case 'prune': + this.notifyListeners('vite:beforePrune', payload); + break; + case 'error': + this.notifyListeners('vite:error', payload); + // TODO support error overlay + break; + default: + logger.warn(`unknown message payload: ${payload}`); + } + } + + handleFarmUpdate(result: RawHmrUpdateResult) { + const immutableModules = eval(result.immutableModules); + const mutableModules = eval(result.mutableModules); + const modules = { ...immutableModules, ...mutableModules }; + this.applyHotUpdates( + { + added: result.added, + changed: result.changed, + removed: result.removed, + boundaries: result.boundaries, + modules, + dynamicResourcesMap: result.dynamicResourcesMap + }, + this.moduleSystem + ); + } +} diff --git a/packages/runtime-plugin-hmr/src/hot-module-state.ts b/packages/runtime-plugin-hmr/src/hot-module-state.ts index 1595bc4cf..adbbba26a 100644 --- a/packages/runtime-plugin-hmr/src/hot-module-state.ts +++ b/packages/runtime-plugin-hmr/src/hot-module-state.ts @@ -1,111 +1,110 @@ -import { HmrUpdateResult } from './types'; -import type { - ModuleSystem, - ModuleInitialization -} from '@farmfe/runtime/src/module-system'; -import { handleErrorSync } from './utils'; - -const REGISTERED_HOT_MODULES = new Map(); +// Farm's HMR client is compatible with Vite, see https://vitejs.dev/guide/api-hmr.html. +// And it's inspired by both Vite and esm-hmr, see https://github.com/FredKSchott/esm-hmr +import { HmrClient } from './hmr-client'; +import { logger } from './logger'; export class HotModuleState { - acceptCallbacks: Array<() => void> = []; + acceptCallbacks: Array<{ deps: string[]; fn: (mods: any[]) => void }> = []; data = {}; id: string; + hmrClient: HmrClient; - constructor(id: string) { + constructor(id: string, hmrClient: HmrClient) { this.id = id; + this.hmrClient = hmrClient; } - accept(callback?: () => void) { - if (callback) { - this.acceptCallbacks.push(callback); + // the same as vite's hot.accept + accept(deps?: any, callback?: (mods: any[]) => void) { + if (typeof deps === 'function' || !deps) { + // self-accept hot.accept(() => {}) + this.acceptCallbacks.push({ + deps: [this.id], + fn: ([mod]) => deps?.(mod) + }); + } else if (typeof deps === 'string') { + // accept a single dependency hot.accept('./dep.js', () => {}) + this.acceptCallbacks.push({ + deps: [deps], + fn: ([mod]) => callback?.(mod) + }); + } else if (Array.isArray(deps)) { + // accept multiple dependencies hot.accept(['./dep1.js', './dep2.js'], () => {}) + this.acceptCallbacks.push({ deps, fn: callback }); + } else { + throw new Error('invalid hot.accept call'); } } - tap = (changeModule: ModuleInitialization) => { - this.acceptCallbacks.map((cb) => { - handleErrorSync(cb, [changeModule], (err) => { - console.error(err); - }); - }); - }; -} - -export function createHotContext(id: string) { - if (REGISTERED_HOT_MODULES.has(id)) { - return REGISTERED_HOT_MODULES.get(id); + dispose(callback: (data: any) => void) { + this.hmrClient.disposeMap.set(this.id, callback); } - const state = new HotModuleState(id); - REGISTERED_HOT_MODULES.set(id, state); - return state; -} - -function removeCssStyles(removed: string[]) { - for (const id of removed) { - const previousStyle = document.querySelector( - `style[data-farm-id="${{ id }}"]` - ); - - if (previousStyle) { - previousStyle.remove(); - } + prune(callback: (data: any[]) => void) { + this.hmrClient.pruneMap.set(this.id, callback); } -} - -export function applyHotUpdates( - result: HmrUpdateResult, - moduleSystem: ModuleSystem -) { - result.changed.forEach((id) => { - console.log(`[Farm HMR] ${id} updated`); - }); - for (const id of result.removed) { - moduleSystem.delete(id); - REGISTERED_HOT_MODULES.delete(id); + acceptExports( + _: string | readonly string[], + _callback: (data: any) => void + ): void { + logger.log('acceptExports is not supported for now'); } - removeCssStyles(result.removed); - - for (const id of result.added) { - moduleSystem.register(id, result.modules[id]); + decline() { + /** does no thing */ } - for (const id of result.changed) { - moduleSystem.update(id, result.modules[id]); - - if (!result.boundaries[id]) { - // do not found boundary module, reload the window - window.location.reload(); - } + invalidate(message?: string) { + this.hmrClient.notifyListeners('vite:invalidate', { + path: this.id, + message + }); + // notify the server to find the boundary starting from the parents of this module + this.send('vite:invalidate', { path: this.id, message }); + logger.log(`invalidate ${this.id}${message ? `: ${message}` : ''}`); } - if (result.dynamicResourcesMap) { - moduleSystem.dynamicModuleResourcesMap = result.dynamicResourcesMap; + on(event: T, cb: (payload: any) => void): void { + const addToMap = (map: Map) => { + const existing = map.get(event) || []; + existing.push(cb); + map.set(event, existing); + }; + addToMap(this.hmrClient.customListenersMap); } - // TODO support accept dependencies change - for (const updated_id of Object.keys(result.boundaries)) { - const chains = result.boundaries[updated_id]; - - for (const chain of chains) { - for (const id of chain) { - moduleSystem.clearCache(id); + off(event: T, cb: (payload: any) => void): void { + const removeFromMap = (map: Map) => { + const existing = map.get(event); + if (existing === undefined) { + return; } - - try { - // require the boundary module - const boundary = chain[chain.length - 1]; - const boundaryExports = moduleSystem.require(boundary); - const hotContext = REGISTERED_HOT_MODULES.get(boundary); - hotContext.tap(boundaryExports); - } catch (err) { - // The boundary module's dependencies may not present in current module system for a multi-page application. We should reload the window in this case. - // See https://github.com/farm-fe/farm/issues/383 - console.error(err); - window.location.reload(); + const pruned = existing.filter((l) => l !== cb); + if (pruned.length === 0) { + map.delete(event); + return; } + map.set(event, pruned); + }; + removeFromMap(this.hmrClient.customListenersMap); + } + + send(event: T, data?: any): void { + if (this.hmrClient.socket.readyState === WebSocket.OPEN) { + this.hmrClient.socket.send( + JSON.stringify({ type: 'custom', event, data }) + ); } } } + +export function createHotContext(id: string, hmrClient: HmrClient) { + if (hmrClient.registeredHotModulesMap.has(id)) { + return hmrClient.registeredHotModulesMap.get(id); + } + + const state = new HotModuleState(id, hmrClient); + hmrClient.registeredHotModulesMap.set(id, state); + return state; +} diff --git a/packages/runtime-plugin-hmr/src/index.ts b/packages/runtime-plugin-hmr/src/index.ts index 0aa24ca7a..55ed2f3ef 100644 --- a/packages/runtime-plugin-hmr/src/index.ts +++ b/packages/runtime-plugin-hmr/src/index.ts @@ -2,67 +2,19 @@ * HMR client as a Farm Runtime Plugin */ import type { FarmRuntimePlugin } from '@farmfe/runtime'; -import { applyHotUpdates, createHotContext } from './hot-module-state'; -import type { RawHmrUpdateResult } from './types'; +import { createHotContext } from './hot-module-state'; +import { HmrClient } from './hmr-client'; -const port = Number(FARM_HMR_PORT || 9000); -// TODO use import.meta to get hostname -const host = - typeof FARM_HMR_HOST === 'boolean' - ? window.location.hostname || 'localhost' - : FARM_HMR_HOST || 'localhost'; - -const path = FARM_HMR_PATH || '/__hmr'; +let hmrClient: HmrClient; export default { name: 'farm-runtime-hmr-client-plugin', bootstrap(moduleSystem) { - console.log('[Farm HMR] connecting to the server...'); - - function connect() { - // setup websocket connection - const socket = new WebSocket(`ws://${host}:${port}${path}`, 'farm_hmr'); - // listen for the message from the server - // when the user save the file, the server will recompile the file(and its dependencies as long as its dependencies are changed) - // after the file is recompiled, the server will generated a update resource and send its id to the client - // the client will use the id to fetch the update resource and apply the update - socket.addEventListener('message', (event) => { - // const data = JSON.parse(event.data) as HmrUpdatePacket; - const result: RawHmrUpdateResult = eval(`(${event.data})`); - const immutableModules = eval(result.immutableModules); - const mutableModules = eval(result.mutableModules); - const modules = { ...immutableModules, ...mutableModules }; - applyHotUpdates( - { - added: result.added, - changed: result.changed, - removed: result.removed, - boundaries: result.boundaries, - modules, - dynamicResourcesMap: result.dynamicResourcesMap - }, - moduleSystem - ); - // import(`/__hmr?id=${data.id}`).then( - // (result: { default: HmrUpdateResult }) => { - // applyHotUpdates(result.default, moduleSystem); - // } - // ); - }); - - socket.addEventListener('open', () => { - console.log('[Farm HMR] connected to the server'); - }); - // TODO use ping/pong to detect the connection is closed, and if the server is online again, reload the page - // socket.addEventListener('close', () => setTimeout(connect, 3000)); - - return socket; - } - - connect(); + hmrClient = new HmrClient(moduleSystem); + hmrClient.connect(); }, moduleCreated(module) { // create a hot context for each module - module.meta.hot = createHotContext(module.id); + module.meta.hot = createHotContext(module.id, hmrClient); } }; diff --git a/packages/runtime-plugin-hmr/src/logger.ts b/packages/runtime-plugin-hmr/src/logger.ts new file mode 100644 index 000000000..63cc570c7 --- /dev/null +++ b/packages/runtime-plugin-hmr/src/logger.ts @@ -0,0 +1,10 @@ +export class Logger { + log(message: string) { + console.log('[Farm HMR]', message); + } + warn(message: string) { + console.warn('[Farm HMR] Warning:', message); + } +} + +export const logger = new Logger(); diff --git a/packages/runtime-plugin-hmr/src/types.ts b/packages/runtime-plugin-hmr/src/types.ts index 803019f6b..8158bb451 100644 --- a/packages/runtime-plugin-hmr/src/types.ts +++ b/packages/runtime-plugin-hmr/src/types.ts @@ -33,3 +33,72 @@ export interface RawHmrUpdateResult { mutableModules: string; dynamicResourcesMap: Record | null; } + +// the same as Vite, see LICENSE. modified by @farmfe +export type HMRPayload = + | FarmHmrPayload + | ConnectedPayload + | UpdatePayload + | FullReloadPayload + | CustomPayload + | ErrorPayload + | PrunePayload; + +export interface FarmHmrPayload { + type: 'farm-update'; + result: RawHmrUpdateResult; +} + +export interface ConnectedPayload { + type: 'connected'; +} + +export interface UpdatePayload { + type: 'update'; + updates: Update[]; +} + +export interface Update { + type: 'js-update' | 'css-update'; + path: string; + acceptedPath: string; + timestamp: number; + /** + * @experimental internal + */ + explicitImportRequired?: boolean | undefined; +} + +export interface PrunePayload { + type: 'prune'; + paths: string[]; +} + +export interface FullReloadPayload { + type: 'full-reload'; + path?: string; +} + +export interface CustomPayload { + type: 'custom'; + event: string; + data?: any; +} + +export interface ErrorPayload { + type: 'error'; + err: { + [name: string]: any; + message: string; + stack: string; + id?: string; + frame?: string; + plugin?: string; + pluginCode?: string; + loc?: { + file?: string; + line: number; + column: number; + }; + }; +} diff --git a/packages/runtime-plugin-hmr/tsconfig.json b/packages/runtime-plugin-hmr/tsconfig.json index 6b88ca3b0..d3ba2eab2 100644 --- a/packages/runtime-plugin-hmr/tsconfig.json +++ b/packages/runtime-plugin-hmr/tsconfig.json @@ -1,6 +1,4 @@ { "extends": "../../tsconfig.base.json", - "exclude": ["node_modules"], - "compilerOptions": {}, - + "exclude": ["node_modules"] } diff --git a/packages/runtime/src/module-system.ts b/packages/runtime/src/module-system.ts index b94b55726..ed0ccc719 100644 --- a/packages/runtime/src/module-system.ts +++ b/packages/runtime/src/module-system.ts @@ -197,8 +197,6 @@ export class ModuleSystem { delete(moduleId: string): boolean { if (this.modules[moduleId]) { - this.cache[moduleId] && this.cache[moduleId].dispose?.(); - this.clearCache(moduleId); delete this.modules[moduleId]; return true; diff --git a/packages/runtime/src/module.ts b/packages/runtime/src/module.ts index e82f2f459..f7c33b149 100644 --- a/packages/runtime/src/module.ts +++ b/packages/runtime/src/module.ts @@ -6,8 +6,6 @@ export class Module { meta: Record; require: (id: string) => any; - dispose?: () => void; - constructor(id: string, require: (id: string) => any) { this.id = id; this.exports = {}; @@ -16,8 +14,4 @@ export class Module { }; this.require = require; } - - onDispose(callback: () => void) { - this.dispose = callback; - } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b62fbac57..31fd39d20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,6 +321,19 @@ importers: specifier: ^18.0.0 version: 18.0.0 + examples/hmr: + dependencies: + core-js: + specifier: ^3.34.0 + version: 3.34.0 + devDependencies: + '@farmfe/cli': + specifier: workspace:^ + version: link:../../packages/cli + '@farmfe/core': + specifier: workspace:* + version: link:../../packages/core + examples/js-plugin-cache: devDependencies: '@farmfe/cli': @@ -867,6 +880,33 @@ importers: specifier: ^2.7.0 version: 2.7.2(solid-js@1.8.5)(vite@4.5.0) + examples/vite-adapter-svelet: + devDependencies: + '@farmfe/cli': + specifier: workspace:^ + version: link:../../packages/cli + '@farmfe/core': + specifier: workspace:* + version: link:../../packages/core + '@sveltejs/adapter-auto': + specifier: ^2.0.0 + version: 2.0.0(@sveltejs/kit@1.27.4) + '@sveltejs/kit': + specifier: ^1.27.4 + version: 1.27.4(svelte@4.2.7)(vite@4.5.0) + svelte: + specifier: ^4.2.7 + version: 4.2.7 + svelte-check: + specifier: ^3.6.0 + version: 3.6.0(svelte@4.2.7) + tslib: + specifier: ^2.4.1 + version: 2.6.2 + typescript: + specifier: ^5.0.0 + version: 5.2.2 + examples/vite-adapter-vue: dependencies: axios: @@ -2508,14 +2548,14 @@ packages: '@commitlint/types': 17.8.1 '@types/node': 20.5.1 chalk: 4.1.2 - cosmiconfig: 8.3.6(typescript@4.9.5) - cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@4.9.5) + cosmiconfig: 8.3.6(typescript@5.2.2) + cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@5.2.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@20.5.1)(typescript@4.9.5) - typescript: 4.9.5 + ts-node: 10.9.1(@types/node@20.5.1)(typescript@5.2.2) + typescript: 5.2.2 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -3216,6 +3256,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@fastify/busboy@2.1.0: + resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} + engines: {node: '>=14'} + dev: true + /@floating-ui/core@1.5.0: resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} dependencies: @@ -3419,7 +3464,6 @@ packages: /@polka/url@1.0.0-next.23: resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} - dev: false /@rc-component/color-picker@1.4.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-vh5EWqnsayZa/JwUznqDaPJz39jznx/YDbyBuVJntv735tKXKwEUZZb2jYEldOg+NKWZwtALjGMrNeGBmqFoEw==} @@ -3553,6 +3597,79 @@ packages: solid-js: 1.8.5 dev: false + /@sveltejs/adapter-auto@2.0.0(@sveltejs/kit@1.27.4): + resolution: {integrity: sha512-b+gkHFZgD771kgV3aO4avHFd7y1zhmMYy9i6xOK7m/rwmwaRO8gnF5zBc0Rgca80B2PMU1bKNxyBTHA14OzUAQ==} + peerDependencies: + '@sveltejs/kit': ^1.0.0 + dependencies: + '@sveltejs/kit': 1.27.4(svelte@4.2.7)(vite@4.5.0) + import-meta-resolve: 2.2.2 + dev: true + + /@sveltejs/kit@1.27.4(svelte@4.2.7)(vite@4.5.0): + resolution: {integrity: sha512-Vxl8Jf0C1+/8i/slsxFOnwJntCBDLueO/O6GJ0390KUnyW3Zs+4ZiIinD+cEcYnJPQQ9CRzVSr9Bn6DbmTn4Dw==} + engines: {node: ^16.14 || >=18} + hasBin: true + requiresBuild: true + peerDependencies: + svelte: ^3.54.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.7)(vite@4.5.0) + '@types/cookie': 0.5.4 + cookie: 0.5.0 + devalue: 4.3.2 + esm-env: 1.0.0 + kleur: 4.1.5 + magic-string: 0.30.5 + mrmime: 1.0.1 + sade: 1.8.1 + set-cookie-parser: 2.6.0 + sirv: 2.0.3 + svelte: 4.2.7 + tiny-glob: 0.2.9 + undici: 5.26.5 + vite: 4.5.0(@types/node@18.18.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.5.3)(svelte@4.2.7)(vite@4.5.0): + resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.2.0 + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.7)(vite@4.5.0) + debug: 4.3.4 + svelte: 4.2.7 + vite: 4.5.0(@types/node@18.18.8) + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte@2.5.3(svelte@4.2.7)(vite@4.5.0): + resolution: {integrity: sha512-erhNtXxE5/6xGZz/M9eXsmI7Pxa6MS7jyTy06zN3Ck++ldrppOnOlJwHHTsMC7DHDQdgUp4NAc4cDNQ9eGdB/w==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.5.3)(svelte@4.2.7)(vite@4.5.0) + debug: 4.3.4 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.5 + svelte: 4.2.7 + svelte-hmr: 0.15.3(svelte@4.2.7) + vite: 4.5.0(@types/node@18.18.8) + vitefu: 0.2.5(vite@4.5.0) + transitivePeerDependencies: + - supports-color + dev: true + /@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.23.2): resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -4822,6 +4939,10 @@ packages: resolution: {integrity: sha512-V9/5u21RHFR1zfdm3rQ6pJUKV+zSSVQt+yq16i1YhdivVzWgPEoKedc3GdT8aFjsqQbakdxuy3FnEdePUQOamQ==} dev: true + /@types/cookie@0.5.4: + resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==} + dev: true + /@types/cookies@0.7.9: resolution: {integrity: sha512-SrGYvhKohd/WSOII0WpflC73RgdJhQoqpwq9q+n/qugNGiDSGYXfHy3QvB4+X+J/gYe27j2fSRnK4B+1A3nvsw==} dependencies: @@ -5051,6 +5172,10 @@ packages: /@types/prop-types@15.7.9: resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==} + /@types/pug@2.0.10: + resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + dev: true + /@types/qs@6.9.9: resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==} dev: true @@ -5839,6 +5964,12 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: @@ -5948,6 +6079,12 @@ packages: - debug dev: false + /axobject-query@3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + dependencies: + dequal: 2.0.3 + dev: true + /b-tween@0.3.3: resolution: {integrity: sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==} dev: false @@ -6523,6 +6660,16 @@ packages: /code-block-writer@12.0.0: resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} + /code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + '@types/estree': 1.0.4 + acorn: 8.11.2 + estree-walker: 3.0.3 + periscopic: 3.1.0 + dev: true + /codepage@1.14.0: resolution: {integrity: sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==} engines: {node: '>=0.8'} @@ -6755,7 +6902,6 @@ packages: /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} - dev: false /cookies@0.8.0: resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==} @@ -6802,10 +6948,15 @@ packages: resolution: {integrity: sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==} requiresBuild: true + /core-js@3.34.0: + resolution: {integrity: sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==} + requiresBuild: true + dev: false + /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - /cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@4.9.5): + /cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==} engines: {node: '>=v14.21.3'} peerDependencies: @@ -6815,9 +6966,9 @@ packages: typescript: '>=4' dependencies: '@types/node': 20.5.1 - cosmiconfig: 8.3.6(typescript@4.9.5) - ts-node: 10.9.1(@types/node@20.5.1)(typescript@4.9.5) - typescript: 4.9.5 + cosmiconfig: 8.3.6(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.5.1)(typescript@5.2.2) + typescript: 5.2.2 dev: true /cosmiconfig@7.1.0: @@ -6857,6 +7008,22 @@ packages: typescript: 4.9.5 dev: true + /cosmiconfig@8.3.6(typescript@5.2.2): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.2.2 + dev: true + /crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -7006,6 +7173,14 @@ packages: - encoding dev: true + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: true + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -7265,6 +7440,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + /default-browser-id@3.0.0: resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} engines: {node: '>=12'} @@ -7336,6 +7516,11 @@ packages: engines: {node: '>= 0.8'} dev: false + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: true + /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -7354,6 +7539,10 @@ packages: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false + /devalue@4.3.2: + resolution: {integrity: sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==} + dev: true + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -7610,6 +7799,10 @@ packages: is-date-object: 1.0.5 is-symbol: 1.0.4 + /es6-promise@3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + dev: true + /esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -7737,6 +7930,10 @@ packages: - supports-color dev: true + /esm-env@1.0.0: + resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + dev: true + /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7779,6 +7976,12 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.4 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -8328,6 +8531,10 @@ packages: dependencies: define-properties: 1.2.1 + /globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + dev: true + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -8340,6 +8547,10 @@ packages: slash: 3.0.0 dev: true + /globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + dev: true + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -8843,6 +9054,12 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /is-reference@3.0.2: + resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + dependencies: + '@types/estree': 1.0.4 + dev: true + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -9313,6 +9530,10 @@ packages: pkg-types: 1.0.3 dev: true + /locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + dev: true + /locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -9480,6 +9701,13 @@ packages: yallist: 4.0.0 dev: true + /magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} @@ -9532,6 +9760,10 @@ packages: resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} dev: false + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -9708,6 +9940,13 @@ packages: engines: {node: '>= 8.0.0'} dev: true + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -9751,10 +9990,14 @@ packages: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} dev: true + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + /mrmime@1.0.1: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} - dev: false /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -10259,6 +10502,14 @@ packages: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true + /periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + dependencies: + '@types/estree': 1.0.4 + estree-walker: 3.0.3 + is-reference: 3.0.2 + dev: true + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -11632,6 +11883,13 @@ packages: align-text: 0.1.4 dev: false + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -11694,6 +11952,13 @@ packages: dependencies: tslib: 2.6.2 + /sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + /safe-array-concat@1.0.1: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} @@ -11723,6 +11988,15 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /sander@0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.11 + mkdirp: 0.5.6 + rimraf: 2.7.1 + dev: true + /sass-embedded-darwin-arm64@1.60.0: resolution: {integrity: sha512-vgHLlLo+HggvmDT60fwMDJjjSUBRp65yZBbaXNEigUjHCAt4ghQ0fsWmcyWRXX8yFAOVH3Ya8qtbUlkvE1e3qw==} engines: {node: '>=14.0.0'} @@ -12035,6 +12309,10 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + /set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + dev: true + /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} engines: {node: '>= 0.4'} @@ -12125,7 +12403,6 @@ packages: '@polka/url': 1.0.0-next.23 mrmime: 1.0.1 totalist: 3.0.1 - dev: false /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -12201,6 +12478,16 @@ packages: '@babel/types': 7.23.0 solid-js: 1.8.5 + /sorcery@0.11.0: + resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} + hasBin: true + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + buffer-crc32: 0.2.13 + minimist: 1.2.8 + sander: 0.5.1 + dev: true + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -12517,6 +12804,108 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /svelte-check@3.6.0(svelte@4.2.7): + resolution: {integrity: sha512-8VfqhfuRJ1sKW+o8isH2kPi0RhjXH1nNsIbCFGyoUHG+ZxVxHYRKcb+S8eaL/1tyj3VGvWYx3Y5+oCUsJgnzcw==} + hasBin: true + peerDependencies: + svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + chokidar: 3.5.3 + fast-glob: 3.3.1 + import-fresh: 3.3.0 + picocolors: 1.0.0 + sade: 1.8.1 + svelte: 4.2.7 + svelte-preprocess: 5.1.1(svelte@4.2.7)(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + dev: true + + /svelte-hmr@0.15.3(svelte@4.2.7): + resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: ^3.19.0 || ^4.0.0 + dependencies: + svelte: 4.2.7 + dev: true + + /svelte-preprocess@5.1.1(svelte@4.2.7)(typescript@5.2.2): + resolution: {integrity: sha512-p/Dp4hmrBW5mrCCq29lEMFpIJT2FZsRlouxEc5qpbOmXRbaFs7clLs8oKPwD3xCFyZfv1bIhvOzpQkhMEVQdMw==} + engines: {node: '>= 14.10.0'} + requiresBuild: true + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.55.0 + sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 + svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 + typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + dependencies: + '@types/pug': 2.0.10 + detect-indent: 6.1.0 + magic-string: 0.27.0 + sorcery: 0.11.0 + strip-indent: 3.0.0 + svelte: 4.2.7 + typescript: 5.2.2 + dev: true + + /svelte@4.2.7: + resolution: {integrity: sha512-UExR1KS7raTdycsUrKLtStayu4hpdV3VZQgM0akX8XbXgLBlosdE/Sf3crOgyh9xIjqSYB3UEBuUlIQKRQX2hg==} + engines: {node: '>=16'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + acorn: 8.11.2 + aria-query: 5.3.0 + axobject-query: 3.2.1 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.2 + locate-character: 3.0.0 + magic-string: 0.30.5 + periscopic: 3.1.0 + dev: true + /svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} dev: true @@ -12640,6 +13029,13 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + /tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + dev: true + /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} dev: false @@ -12723,7 +13119,6 @@ packages: /totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - dev: false /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -12751,7 +13146,7 @@ packages: code-block-writer: 12.0.0 dev: false - /ts-node@10.9.1(@types/node@20.5.1)(typescript@4.9.5): + /ts-node@10.9.1(@types/node@20.5.1)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -12777,7 +13172,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.9.5 + typescript: 5.2.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -12964,6 +13359,13 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /undici@5.26.5: + resolution: {integrity: sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.0 + dev: true + /unimport@3.4.0(rollup@3.29.4): resolution: {integrity: sha512-M/lfFEgufIT156QAr/jWHLUn55kEmxBBiQsMxvRSIbquwmeJEyQYgshHDEvQDWlSJrVOOTAgnJ3FvlsrpGkanA==} dependencies: diff --git a/rust-plugins/react/src/react_refresh.rs b/rust-plugins/react/src/react_refresh.rs index 1b8caa270..bf4bd6bd9 100644 --- a/rust-plugins/react/src/react_refresh.rs +++ b/rust-plugins/react/src/react_refresh.rs @@ -21,7 +21,11 @@ window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; const POST_CODE: &str = r#" window.$RefreshReg$ = prevRefreshReg; window.$RefreshSig$ = prevRefreshSig; -module.meta.hot.accept(); + +if (import.meta.hot) { + import.meta.hot.accept(); +} + RefreshRuntime.performReactRefresh(); "#;