Skip to content

Commit

Permalink
Refactor code to transformer and update example for better rerender b…
Browse files Browse the repository at this point in the history
…ehaviour
  • Loading branch information
JakeLane committed Aug 1, 2024
1 parent 8225b85 commit 689a5f1
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 99 deletions.
24 changes: 21 additions & 3 deletions packages/examples/phases/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,21 +13,39 @@ const Tier3Instance1 = deferredLoadComponent(DeferredTier3);
const Tier3Instance2 = deferredLoadComponent(DeferredTier3);

function App() {
const [count, setCount] = useState(0);

return (
<>
<button
onClick={() => {
setCount(count + 1);
}}
>
{count}
</button>
<div>App</div>
<Tier1 />
<Suspense fallback={<div>Loading Tier 2...</div>}>
<Tier2 />
<Tier2 enabled />
</Suspense>
<Suspense fallback={<div>Loading Tier 3 instance 1...</div>}>
<Tier3Instance1 />
</Suspense>
<Suspense fallback={<div>Loading Tier 3 instance 2...</div>}>
<Tier3Instance2 />
</Suspense>
<div>
Tier3Instance1 and Tier3Instance2 are{' '}
{Tier3Instance1 === Tier3Instance2 ? 'the same' : 'different'}
</div>
</>
);
}

ReactDOM.render(<App />, document.getElementById('app'));
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById('app'),
);
5 changes: 3 additions & 2 deletions packages/examples/phases/src/tier2.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';

export default function Tier2() {
return <div>Tier 2</div>;
export default function Tier2({enabled}: {enabled: boolean}) {
if (enabled) return <div>Tier 2</div>;
throw new Error('Enabled prop missing');
}
60 changes: 37 additions & 23 deletions packages/examples/phases/src/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import React, {FC} from 'react';
import React, {
ComponentType,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
forwardRef,
} from 'react';

let loaderMap = new WeakMap<DeferredImport<any>, Promise<any>>();
let componentMap = new WeakMap<DeferredImport<any>, any>();
export function deferredLoadComponent<P extends {[k: string]: any} | undefined>(
resource: DeferredImport<{default: ComponentType<P>}>,
): ForwardRefExoticComponent<
PropsWithoutRef<P> & RefAttributes<ComponentType<P>>
> {
// Create a deferred component map in the global context, so we can reuse the components everywhere
if (!globalThis.deferredComponentMap) {
globalThis.deferredComponentMap = new WeakMap<DeferredImport<any>, any>();
}

export function deferredLoadComponent<T>(
resource: DeferredImport<T extends {default: any} ? T : never>,
): 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<ComponentType<P>, P>(function DeferredComponent(
props,
ref,
) {
if (Component) {
return <Component {...props} />;
return <Component {...props} ref={ref} />;
} 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;
}
24 changes: 7 additions & 17 deletions packages/packagers/js/src/ScopeHoistingPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 0 additions & 27 deletions packages/packagers/js/src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
55 changes: 51 additions & 4 deletions packages/transformers/js/core/src/dependency_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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<DependencyDescriptor>,
ignore_mark: swc_core::common::Mark,
unresolved_mark: swc_core::common::Mark,
config: &'a Config,
diagnostics: &'a mut Vec<Diagnostic>,
) -> impl Fold + 'a {
DependencyCollector {
) -> (Module, bool) {
let mut fold = DependencyCollector {
source_map,
items,
in_try: false,
Expand All @@ -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> {
Expand All @@ -163,6 +172,7 @@ struct DependencyCollector<'a> {
config: &'a Config,
diagnostics: &'a mut Vec<Diagnostic>,
import_meta: Option<ast::VarDecl>,
needs_tier_helpers: bool,
}

impl<'a> DependencyCollector<'a> {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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),
}
Expand Down
20 changes: 10 additions & 10 deletions packages/transformers/js/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ pub struct TransformResult {
pub used_env: HashSet<swc_core::ecma::atoms::JsWord>,
pub has_node_replacements: bool,
pub is_constant_module: bool,
pub needs_tier_helpers: bool,
}

fn targets_to_versions(targets: &Option<HashMap<String, String>>) -> Option<Versions> {
Expand Down Expand Up @@ -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));

Expand Down
Loading

0 comments on commit 689a5f1

Please sign in to comment.