From 689a5f1250d4a84834f0fb942d294fc0f576c257 Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Thu, 1 Aug 2024 17:29:19 +1000 Subject: [PATCH] Refactor code to transformer and update example for better rerender behaviour --- packages/examples/phases/src/index.tsx | 24 +++++++- packages/examples/phases/src/tier2.tsx | 5 +- packages/examples/phases/src/utils.tsx | 60 ++++++++++++------- .../packagers/js/src/ScopeHoistingPackager.js | 24 +++----- packages/packagers/js/src/helpers.js | 27 --------- .../js/core/src/dependency_collector.rs | 55 +++++++++++++++-- packages/transformers/js/core/src/lib.rs | 20 +++---- packages/transformers/js/src/JSTransformer.js | 42 +++++++++---- packages/transformers/js/src/tier-helpers.js | 23 +++++++ 9 files changed, 181 insertions(+), 99 deletions(-) create mode 100644 packages/transformers/js/src/tier-helpers.js diff --git a/packages/examples/phases/src/index.tsx b/packages/examples/phases/src/index.tsx index 34acdd33562..b08a0ac1db6 100644 --- a/packages/examples/phases/src/index.tsx +++ b/packages/examples/phases/src/index.tsx @@ -1,4 +1,4 @@ -import React, {Suspense} from 'react'; +import React, {StrictMode, Suspense, useState} from 'react'; import ReactDOM from 'react-dom'; import Tier1 from './tier1'; @@ -13,12 +13,21 @@ const Tier3Instance1 = deferredLoadComponent(DeferredTier3); const Tier3Instance2 = deferredLoadComponent(DeferredTier3); function App() { + const [count, setCount] = useState(0); + return ( <> +
App
Loading Tier 2...}> - + Loading Tier 3 instance 1...}> @@ -26,8 +35,17 @@ function App() { Loading Tier 3 instance 2...}> +
+ Tier3Instance1 and Tier3Instance2 are{' '} + {Tier3Instance1 === Tier3Instance2 ? 'the same' : 'different'} +
); } -ReactDOM.render(, document.getElementById('app')); +ReactDOM.render( + + + , + document.getElementById('app'), +); diff --git a/packages/examples/phases/src/tier2.tsx b/packages/examples/phases/src/tier2.tsx index 0427df1b229..3428b1c91cf 100644 --- a/packages/examples/phases/src/tier2.tsx +++ b/packages/examples/phases/src/tier2.tsx @@ -1,5 +1,6 @@ import React from 'react'; -export default function Tier2() { - return
Tier 2
; +export default function Tier2({enabled}: {enabled: boolean}) { + if (enabled) return
Tier 2
; + throw new Error('Enabled prop missing'); } diff --git a/packages/examples/phases/src/utils.tsx b/packages/examples/phases/src/utils.tsx index a7b0214ad4a..625f04b7a47 100644 --- a/packages/examples/phases/src/utils.tsx +++ b/packages/examples/phases/src/utils.tsx @@ -1,31 +1,45 @@ -import React, {FC} from 'react'; +import React, { + ComponentType, + ForwardRefExoticComponent, + PropsWithoutRef, + RefAttributes, + forwardRef, +} from 'react'; -let loaderMap = new WeakMap, Promise>(); -let componentMap = new WeakMap, any>(); +export function deferredLoadComponent

( + resource: DeferredImport<{default: ComponentType

}>, +): ForwardRefExoticComponent< + PropsWithoutRef

& RefAttributes> +> { + // Create a deferred component map in the global context, so we can reuse the components everywhere + if (!globalThis.deferredComponentMap) { + globalThis.deferredComponentMap = new WeakMap, any>(); + } -export function deferredLoadComponent( - resource: DeferredImport, -): FC { - if (!loaderMap.has(resource)) { - loaderMap.set( - resource, - new Promise(resolve => { - resource.onReady(component => { - componentMap.set(resource, component); - resolve(component); - }); - }), - ); + if (globalThis.deferredComponentMap.has(resource)) { + return globalThis.deferredComponentMap.get(resource); } - return function WrappedComponent(props) { - const Component = componentMap.get(resource); + let Component: ComponentType | undefined; + const loader = new Promise(resolve => { + resource.onReady(loaded => { + Component = loaded; + resolve(loaded); + }); + }); + + const wrapper = forwardRef, P>(function DeferredComponent( + props, + ref, + ) { if (Component) { - return ; + return ; } else { - throw ( - loaderMap.get(resource) ?? new Error(`Loader map did not have resource`) - ); + throw loader; } - }; + }); + + // Store in weakmap so we only have one instance + globalThis.deferredComponentMap.set(resource, wrapper); + return wrapper; } diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index d2b0cd75da4..b7f129b16f3 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -741,23 +741,13 @@ ${code} } let symbol = this.getSymbolResolution(asset, resolved, imported, dep); - - if ( - this.options.featureFlags.tieredImports && - dep.priority === 'tier' - ) { - // Wrap tiered import symbols with tier helper - replacements.set(local, `$parcel$tier(${symbol})`); - this.usedHelpers.add('$parcel$tier'); - } else { - replacements.set( - local, - // If this was an internalized async asset, wrap in a Promise.resolve. - asyncResolution?.type === 'asset' - ? `Promise.resolve(${symbol})` - : symbol, - ); - } + replacements.set( + local, + // If this was an internalized async asset, wrap in a Promise.resolve. + asyncResolution?.type === 'asset' + ? `Promise.resolve(${symbol})` + : symbol, + ); } // Async dependencies need a namespace object even if all used symbols were statically analyzed. diff --git a/packages/packagers/js/src/helpers.js b/packages/packagers/js/src/helpers.js index 4218a429f1a..de455c32c57 100644 --- a/packages/packagers/js/src/helpers.js +++ b/packages/packagers/js/src/helpers.js @@ -160,37 +160,10 @@ function $parcel$defineInteropFlag(a) { } `; -const $parcel$tier = ` -function $parcel$tier(loader) { - var listeners = new Set(); - var resolved = false; - if (loader instanceof Promise) { - loader.then((mod) => { - resolved = true; - for (let listener of listeners) { - listener?.(mod.default); - } - }); - } else { - resolved = true; - } - return { - onReady: (listener) => { - if (resolved) { - listener(loader.default); - } else { - listeners.add(listener); - } - }, - }; -} -`; - export const helpers = { $parcel$export, $parcel$exportWildcard, $parcel$interopDefault, $parcel$global, $parcel$defineInteropFlag, - $parcel$tier, }; diff --git a/packages/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index 4cac51fa9a9..c28239c9204 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -4,6 +4,8 @@ use std::fmt; use std::hash::Hash; use std::hash::Hasher; use std::path::Path; +use swc_core::ecma::ast::MemberExpr; +use swc_core::ecma::ast::Module; use swc_core::ecma::utils::stack_size::maybe_grow_default; use path_slash::PathBufExt; @@ -18,6 +20,7 @@ use swc_core::ecma::ast::MemberProp; use swc_core::ecma::ast::{self}; use swc_core::ecma::atoms::js_word; use swc_core::ecma::atoms::JsWord; +use swc_core::ecma::utils::ExprFactory; use swc_core::ecma::visit::Fold; use swc_core::ecma::visit::FoldWith; @@ -131,14 +134,15 @@ pub struct DependencyDescriptor { /// This pass collects dependencies in a module and compiles references as needed to work with Parcel's JSRuntime. pub fn dependency_collector<'a>( + module: Module, source_map: &'a SourceMap, items: &'a mut Vec, ignore_mark: swc_core::common::Mark, unresolved_mark: swc_core::common::Mark, config: &'a Config, diagnostics: &'a mut Vec, -) -> impl Fold + 'a { - DependencyCollector { +) -> (Module, bool) { + let mut fold = DependencyCollector { source_map, items, in_try: false, @@ -149,7 +153,12 @@ pub fn dependency_collector<'a>( config, diagnostics, import_meta: None, - } + needs_tier_helpers: false, + }; + + let module = module.fold_with(&mut fold); + + (module, fold.needs_tier_helpers) } struct DependencyCollector<'a> { @@ -163,6 +172,7 @@ struct DependencyCollector<'a> { config: &'a Config, diagnostics: &'a mut Vec, import_meta: Option, + needs_tier_helpers: bool, } impl<'a> DependencyCollector<'a> { @@ -329,6 +339,27 @@ impl<'a> Fold for DependencyCollector<'a> { ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Var(Box::new(decl)))), ); } + + if self.needs_tier_helpers { + res.body.insert( + 0, + ast::ModuleItem::Stmt(ast::Stmt::Decl(ast::Decl::Var(Box::new(ast::VarDecl { + span: DUMMY_SP, + kind: ast::VarDeclKind::Var, + decls: vec![ast::VarDeclarator { + span: DUMMY_SP, + name: ast::Pat::Ident(ast::Ident::new("parcel$tier$".into(), DUMMY_SP).into()), + init: Some(Box::new(ast::Expr::Call(crate::utils::create_require( + "@parcel/transformer-js/src/tier-helpers.js".into(), + self.unresolved_mark, + )))), + definite: false, + }], + declare: false, + })))), + ); + } + res } @@ -799,6 +830,9 @@ impl<'a> Fold for DependencyCollector<'a> { }); } + // Track that we need to add the dependency to the asset + self.needs_tier_helpers = true; + // Convert to require without scope hoisting if !self.config.scope_hoist && !self.config.standalone { let name = match &self.config.source_type { @@ -815,7 +849,20 @@ impl<'a> Fold for DependencyCollector<'a> { let rewritten_call = rewrite_require_specifier(call, self.unresolved_mark); self.require_node = Some(rewritten_call.clone()); - rewritten_call + // Wrap with the parcelTiers helper + ast::CallExpr { + span: DUMMY_SP, + type_args: None, + args: vec![rewritten_call.as_arg()], + callee: ast::Callee::Expr(Box::new(Member(MemberExpr { + obj: Box::new(ast::Expr::Ident(ast::Ident::new( + "parcel$tier$".into(), + DUMMY_SP, + ))), + prop: MemberProp::Ident(ast::Ident::new("load".into(), DUMMY_SP)), + span: DUMMY_SP, + }))), + } } _ => node.fold_children_with(self), } diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 33e1395ed7a..f3d18f305fa 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -145,6 +145,7 @@ pub struct TransformResult { pub used_env: HashSet, pub has_node_replacements: bool, pub is_constant_module: bool, + pub needs_tier_helpers: bool, } fn targets_to_versions(targets: &Option>) -> Option { @@ -459,17 +460,16 @@ pub fn transform( }; let ignore_mark = Mark::fresh(Mark::root()); - let module = module.fold_with( - // Collect dependencies - &mut dependency_collector( - &source_map, - &mut result.dependencies, - ignore_mark, - unresolved_mark, - &config, - &mut diagnostics, - ), + let (module, needs_tier_helpers) = dependency_collector( + module, + &source_map, + &mut result.dependencies, + ignore_mark, + unresolved_mark, + &config, + &mut diagnostics, ); + result.needs_tier_helpers = needs_tier_helpers; diagnostics.extend(error_buffer_to_diagnostics(&error_buffer, &source_map)); diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index 611e25cb48c..d70e8c63cd8 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -407,19 +407,7 @@ export default (new Transformer({ } let macroAssets = []; - let { - dependencies, - code: compiledCode, - map, - shebang, - hoist_result, - symbol_result, - needs_esm_helpers, - diagnostics, - used_env, - has_node_replacements, - is_constant_module, - } = await (transformAsync || transform)({ + let x = await (transformAsync || transform)({ filename: asset.filePath, code, module_id: asset.id, @@ -577,6 +565,21 @@ export default (new Transformer({ tier_imports: options.featureFlags.tieredImports, }); + let { + dependencies, + code: compiledCode, + map, + shebang, + hoist_result, + symbol_result, + needs_esm_helpers, + needs_tier_helpers, + diagnostics, + used_env, + has_node_replacements, + is_constant_module, + } = x; + if (is_constant_module) { asset.meta.isConstantModule = true; } @@ -1070,6 +1073,19 @@ export default (new Transformer({ } } + if (needs_tier_helpers) { + asset.addDependency({ + specifier: '@parcel/transformer-js/src/tier-helpers.js', + specifierType: 'esm', + resolveFrom: __filename, + env: { + includeNodeModules: { + '@parcel/transformer-js': true, + }, + }, + }); + } + asset.type = 'js'; asset.setBuffer(compiledCode); diff --git a/packages/transformers/js/src/tier-helpers.js b/packages/transformers/js/src/tier-helpers.js new file mode 100644 index 00000000000..b9e1a944575 --- /dev/null +++ b/packages/transformers/js/src/tier-helpers.js @@ -0,0 +1,23 @@ +export function load(loader) { + var listeners = new Set(); + var resolved = false; + if (loader instanceof Promise) { + loader.then(mod => { + resolved = true; + for (let listener of listeners) { + listener?.(mod.default); + } + }); + } else { + resolved = true; + } + return { + onReady: listener => { + if (resolved) { + listener(loader.default); + } else { + listeners.add(listener); + } + }, + }; +}