From 8225b851b2be1fc069747e0ff5f1700368f87212 Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Tue, 30 Jul 2024 10:55:20 +1000 Subject: [PATCH] Refactor to use packager to handle runtime code --- .../transformer/conversion/dependency_kind.rs | 8 ++-- .../bundlers/default/src/DefaultBundler.js | 14 ++++--- packages/core/feature-flags/src/types.js | 4 +- packages/examples/phases/phases.d.ts | 5 +-- packages/examples/phases/src/index.tsx | 31 ++++++++------ packages/examples/phases/src/lazy.tsx | 6 +-- packages/examples/phases/src/phase1.tsx | 5 --- packages/examples/phases/src/phase2.tsx | 5 --- packages/examples/phases/src/phase3.tsx | 5 --- packages/examples/phases/src/tier1.tsx | 5 +++ packages/examples/phases/src/tier2.tsx | 5 +++ packages/examples/phases/src/tier3.tsx | 5 +++ packages/examples/phases/src/utils.tsx | 40 +++++++++++-------- .../packagers/js/src/ScopeHoistingPackager.js | 29 +++++++++----- packages/packagers/js/src/helpers.js | 27 +++++++++++++ packages/runtimes/js/src/JSRuntime.js | 15 +------ .../js/src/helpers/browser/tier-loader.js | 23 ----------- .../js/core/src/dependency_collector.rs | 18 ++++----- packages/transformers/js/src/JSTransformer.js | 4 +- 19 files changed, 133 insertions(+), 121 deletions(-) delete mode 100644 packages/examples/phases/src/phase1.tsx delete mode 100644 packages/examples/phases/src/phase2.tsx delete mode 100644 packages/examples/phases/src/phase3.tsx create mode 100644 packages/examples/phases/src/tier1.tsx create mode 100644 packages/examples/phases/src/tier2.tsx create mode 100644 packages/examples/phases/src/tier3.tsx delete mode 100644 packages/runtimes/js/src/helpers/browser/tier-loader.js diff --git a/crates/parcel_plugin_transformer_js/src/transformer/conversion/dependency_kind.rs b/crates/parcel_plugin_transformer_js/src/transformer/conversion/dependency_kind.rs index 0ae50791ffd..c4b02543dbf 100644 --- a/crates/parcel_plugin_transformer_js/src/transformer/conversion/dependency_kind.rs +++ b/crates/parcel_plugin_transformer_js/src/transformer/conversion/dependency_kind.rs @@ -15,8 +15,8 @@ pub(crate) fn convert_priority( DependencyKind::Export => Priority::Sync, DependencyKind::Require => Priority::Sync, DependencyKind::File => Priority::Sync, - DependencyKind::DeferredForDisplayTierImport => Priority::Tier, - DependencyKind::DeferredTierImport => Priority::Tier, + DependencyKind::DeferredForDisplayImport => Priority::Tier, + DependencyKind::DeferredImport => Priority::Tier, } } @@ -35,8 +35,8 @@ pub(crate) fn convert_specifier_type( DependencyKind::Worklet => SpecifierType::Url, DependencyKind::Url => SpecifierType::Url, DependencyKind::File => SpecifierType::Custom, - DependencyKind::DeferredForDisplayTierImport => SpecifierType::Esm, - DependencyKind::DeferredTierImport => SpecifierType::Esm, + DependencyKind::DeferredForDisplayImport => SpecifierType::Esm, + DependencyKind::DeferredImport => SpecifierType::Esm, } } diff --git a/packages/bundlers/default/src/DefaultBundler.js b/packages/bundlers/default/src/DefaultBundler.js index c60e9d9ffc6..3179c355b1a 100644 --- a/packages/bundlers/default/src/DefaultBundler.js +++ b/packages/bundlers/default/src/DefaultBundler.js @@ -278,8 +278,7 @@ function decorateLegacyGraph( ); for (let incomingDep of incomingDeps) { if ( - (incomingDep.priority === 'lazy' || - incomingDep.priority === 'tier') && + incomingDep.priority === 'lazy' && incomingDep.specifierType !== 'url' && bundle.hasDependency(incomingDep) ) { @@ -296,7 +295,7 @@ function decorateLegacyGraph( let incomingDeps = bundleGraph.getIncomingDependencies(manualSharedAsset); for (let incomingDep of incomingDeps) { if ( - (incomingDep.priority === 'lazy' || incomingDep.priority === 'tier') && + incomingDep.priority === 'lazy' && incomingDep.specifierType !== 'url' ) { let bundles = bundleGraph.getBundlesWithDependency(incomingDep); @@ -497,7 +496,7 @@ function createIdealGraph( if ( node.type === 'dependency' && - (node.value.priority === 'lazy' || node.value.priority === 'tier') && + node.value.priority === 'lazy' && parentAsset ) { // Don't walk past the bundle group assets @@ -586,7 +585,6 @@ function createIdealGraph( } if ( dependency.priority === 'lazy' || - dependency.priority === 'tier' || childAsset.bundleBehavior === 'isolated' // An isolated Dependency, or Bundle must contain all assets it needs to load. ) { if (bundleId == null) { @@ -848,6 +846,7 @@ function createIdealGraph( if ( dependency.priority !== 'sync' && + dependency.priority !== 'tier' && dependencyBundleGraph.hasContentKey(dependency.id) ) { let assets = assetGraph.getDependencyAssets(dependency); @@ -875,7 +874,10 @@ function createIdealGraph( } } - if (dependency.priority !== 'sync') { + if ( + dependency.priority !== 'sync' && + dependency.priority !== 'tier' + ) { actions.skipChildren(); } return; diff --git a/packages/core/feature-flags/src/types.js b/packages/core/feature-flags/src/types.js index 2681152fb4d..9bb55324c44 100644 --- a/packages/core/feature-flags/src/types.js +++ b/packages/core/feature-flags/src/types.js @@ -14,7 +14,9 @@ export type FeatureFlags = {| */ +parcelV3: boolean, /** - * Tiered imports API + * Enable tier imports + * + * Tier imports allow developers to have control over when code is loaded */ +tieredImports: boolean, |}; diff --git a/packages/examples/phases/phases.d.ts b/packages/examples/phases/phases.d.ts index 1101d1c470b..c914e1855df 100644 --- a/packages/examples/phases/phases.d.ts +++ b/packages/examples/phases/phases.d.ts @@ -1,9 +1,8 @@ type ModuleRef<_> = string; type ErrorMessage = 'You must annotate type with ""'; -interface DeferredImport { - onReady(resource: () => void): () => void; - mod: T | null; +interface DeferredImport { + onReady(resource: (mod: T['default']) => void): void; } declare function importDeferredForDisplay( diff --git a/packages/examples/phases/src/index.tsx b/packages/examples/phases/src/index.tsx index b439f694433..34acdd33562 100644 --- a/packages/examples/phases/src/index.tsx +++ b/packages/examples/phases/src/index.tsx @@ -1,25 +1,30 @@ -import React, {FC, Suspense, useEffect} from 'react'; +import React, {Suspense} from 'react'; import ReactDOM from 'react-dom'; -import ModulePhase1 from './phase1'; +import Tier1 from './tier1'; +const DeferredTier2 = + importDeferredForDisplay('./tier2'); +const DeferredTier3 = importDeferred('./tier3'); + import {deferredLoadComponent} from './utils'; -const Phase2 = deferredLoadComponent( - importDeferredForDisplay('./phase2'), -); -const Phase3 = deferredLoadComponent( - importDeferred('./phase3'), -); + +const Tier2 = deferredLoadComponent(DeferredTier2); +const Tier3Instance1 = deferredLoadComponent(DeferredTier3); +const Tier3Instance2 = deferredLoadComponent(DeferredTier3); function App() { return ( <>
App
- - Loading...}> - + + Loading Tier 2...}> + + + Loading Tier 3 instance 1...}> + - Loading...}> - + Loading Tier 3 instance 2...}> + ); diff --git a/packages/examples/phases/src/lazy.tsx b/packages/examples/phases/src/lazy.tsx index dcb33628453..03031ad99ee 100644 --- a/packages/examples/phases/src/lazy.tsx +++ b/packages/examples/phases/src/lazy.tsx @@ -1,5 +1,5 @@ import React from 'react'; -const Lazy = () =>
Lazy
; - -export default Lazy; +export default function Lazy() { + return
Lazy
; +} diff --git a/packages/examples/phases/src/phase1.tsx b/packages/examples/phases/src/phase1.tsx deleted file mode 100644 index 09ccd65dfae..00000000000 --- a/packages/examples/phases/src/phase1.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -const Phase1 = () =>
Phase 1
; - -export default Phase1; diff --git a/packages/examples/phases/src/phase2.tsx b/packages/examples/phases/src/phase2.tsx deleted file mode 100644 index beccaa153f3..00000000000 --- a/packages/examples/phases/src/phase2.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -const ModulePhase2 = () =>
Phase 2
; - -export default ModulePhase2; diff --git a/packages/examples/phases/src/phase3.tsx b/packages/examples/phases/src/phase3.tsx deleted file mode 100644 index 874982b8523..00000000000 --- a/packages/examples/phases/src/phase3.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -const ModulePhase3 = () =>
Phase 3
; - -export default ModulePhase3; diff --git a/packages/examples/phases/src/tier1.tsx b/packages/examples/phases/src/tier1.tsx new file mode 100644 index 00000000000..6a3b9bf8830 --- /dev/null +++ b/packages/examples/phases/src/tier1.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Tier1() { + return
Tier 1
; +} diff --git a/packages/examples/phases/src/tier2.tsx b/packages/examples/phases/src/tier2.tsx new file mode 100644 index 00000000000..0427df1b229 --- /dev/null +++ b/packages/examples/phases/src/tier2.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Tier2() { + return
Tier 2
; +} diff --git a/packages/examples/phases/src/tier3.tsx b/packages/examples/phases/src/tier3.tsx new file mode 100644 index 00000000000..f0d82f99775 --- /dev/null +++ b/packages/examples/phases/src/tier3.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Tier3() { + return
Tier 3
; +} diff --git a/packages/examples/phases/src/utils.tsx b/packages/examples/phases/src/utils.tsx index 1afd2afab3a..a7b0214ad4a 100644 --- a/packages/examples/phases/src/utils.tsx +++ b/packages/examples/phases/src/utils.tsx @@ -1,25 +1,31 @@ -import React, {FC, useEffect} from 'react'; +import React, {FC} from 'react'; + +let loaderMap = new WeakMap, Promise>(); +let componentMap = new WeakMap, any>(); export function deferredLoadComponent( - Resource: DeferredImport, + resource: DeferredImport, ): FC { - let loaded = false; - let cleanUp: undefined | (() => void); + if (!loaderMap.has(resource)) { + loaderMap.set( + resource, + new Promise(resolve => { + resource.onReady(component => { + componentMap.set(resource, component); + resolve(component); + }); + }), + ); + } + return function WrappedComponent(props) { - useEffect(() => { - return () => { - cleanUp?.(); - }; - }, []); - if (loaded) { - return ; + const Component = componentMap.get(resource); + if (Component) { + return ; } else { - throw new Promise(resolve => { - cleanUp = Resource.onReady(() => { - loaded = true; - resolve(Resource); - }); - }); + throw ( + loaderMap.get(resource) ?? new Error(`Loader map did not have resource`) + ); } }; } diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index 26b6457ca6a..d2b0cd75da4 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -741,22 +741,29 @@ ${code} } let symbol = this.getSymbolResolution(asset, resolved, imported, dep); - replacements.set( - local, - // If this was an internalized async asset, wrap in a Promise.resolve. - asyncResolution?.type === 'asset' - ? `Promise.resolve(${symbol})` - : symbol, - ); + + 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, + ); + } } // Async dependencies need a namespace object even if all used symbols were statically analyzed. // This is recorded in the promiseSymbol meta property set by the transformer rather than in // symbols so that we don't mark all symbols as used. - if ( - (dep.priority === 'lazy' || dep.priority === 'tier') && - dep.meta.promiseSymbol - ) { + if (dep.priority === 'lazy' && dep.meta.promiseSymbol) { let promiseSymbol = dep.meta.promiseSymbol; invariant(typeof promiseSymbol === 'string'); let symbol = this.getSymbolResolution(asset, resolved, '*', dep); diff --git a/packages/packagers/js/src/helpers.js b/packages/packagers/js/src/helpers.js index de455c32c57..4218a429f1a 100644 --- a/packages/packagers/js/src/helpers.js +++ b/packages/packagers/js/src/helpers.js @@ -160,10 +160,37 @@ 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/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js index 31b5394f4a3..d66e1d6b530 100644 --- a/packages/runtimes/js/src/JSRuntime.js +++ b/packages/runtimes/js/src/JSRuntime.js @@ -315,7 +315,7 @@ function getDependencies(bundle: NamedBundle): {| let dependency = node.value; if ( - (dependency.priority === 'lazy' || dependency.priority === 'tier') && + dependency.priority === 'lazy' && dependency.specifierType !== 'url' ) { asyncDependencies.push(dependency); @@ -475,7 +475,6 @@ function getLoaderRuntime({ loaderCode = `(${loaderCode})`; } - let needsTierPrelude = false; if (mainBundle.type === 'js') { let parcelRequire = bundle.env.shouldScopeHoist ? 'parcelRequire' @@ -483,15 +482,6 @@ function getLoaderRuntime({ loaderCode += `.then(() => ${parcelRequire}('${bundleGraph.getAssetPublicId( bundleGraph.getAssetById(bundleGroup.entryAssetId), )}'))`; - - if ( - options.featureFlags.tieredImports && - (dependency.meta.kind === 'DeferredForDisplayTierImport' || - dependency.meta.kind === 'DeferredTierImport') - ) { - loaderCode = `tier(${loaderCode})`; - needsTierPrelude = true; - } } let code = []; @@ -499,9 +489,6 @@ function getLoaderRuntime({ if (needsEsmLoadPrelude) { code.push(`let load = require('./helpers/browser/esm-js-loader');`); } - if (needsTierPrelude) { - code.push(`let tier = require('./helpers/browser/tier-loader');`); - } code.push(`module.exports = ${loaderCode};`); diff --git a/packages/runtimes/js/src/helpers/browser/tier-loader.js b/packages/runtimes/js/src/helpers/browser/tier-loader.js deleted file mode 100644 index a0ee550bd9d..00000000000 --- a/packages/runtimes/js/src/helpers/browser/tier-loader.js +++ /dev/null @@ -1,23 +0,0 @@ -function tier(loader) { - const listeners = new Set(); - let mod = null; - loader.then(loaded => { - mod = loaded; - for (let listener of listeners) { - listener?.(); - } - }); - return { - get mod() { - return mod; - }, - onReady: listener => { - listeners.add(listener); - return () => { - listeners.delete(listener); - }; - }, - }; -} - -module.exports = tier; diff --git a/packages/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index 176177ace6c..4cac51fa9a9 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -49,16 +49,16 @@ pub enum DependencyKind { /// import('./dependency').then(({x}) => {/* ... */}); /// ``` DynamicImport, - /// Corresponds to a for display (tier) import statement + /// Corresponds to a deferred for display import statement /// ```skip - /// const {x} = importDeferredForDisplay('./dependency'); + /// const DependencyDeferredForDisplay = importDeferredForDisplay('./dependency'); /// ``` - DeferredForDisplayTierImport, - /// Corresponds to a deferred (tier) import statement + DeferredForDisplayImport, + /// Corresponds to a deferred import statement /// ```skip - /// const {x} = importDeferred('./dependency'); + /// const DependencyDeferred = importDeferred('./dependency'); /// ``` - DeferredTierImport, + DeferredImport, /// Corresponds to CJS require statements /// ```skip /// const {x} = require('./dependency'); @@ -439,10 +439,10 @@ impl<'a> Fold for DependencyCollector<'a> { Callee::Expr(expr) => { match &**expr { Ident(ident) if ident.sym.to_string().as_str() == "importDeferredForDisplay" => { - DependencyKind::DeferredForDisplayTierImport + DependencyKind::DeferredForDisplayImport } Ident(ident) if ident.sym.to_string().as_str() == "importDeferred" => { - DependencyKind::DeferredTierImport + DependencyKind::DeferredImport } Ident(ident) => { // Bail if defined in scope @@ -780,7 +780,7 @@ impl<'a> Fold for DependencyCollector<'a> { { rewrite_require_specifier(node, self.unresolved_mark) } - DependencyKind::DeferredForDisplayTierImport | DependencyKind::DeferredTierImport => { + DependencyKind::DeferredForDisplayImport | DependencyKind::DeferredImport => { if !self.config.tier_imports { return node.fold_children_with(self); } diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index 945ea2ebda1..611e25cb48c 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -849,8 +849,8 @@ export default (new Transformer({ let priority; switch (dep.kind) { - case 'DeferredForDisplayTierImport': - case 'DeferredTierImport': + case 'DeferredForDisplayImport': + case 'DeferredImport': priority = 'tier'; break; case 'DynamicImport':