From f84f3905634ad00d3e25697253e4d3754fca82e2 Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Tue, 23 Jul 2024 11:15:12 +1000 Subject: [PATCH 01/10] Add no-op tier import API --- crates/parcel_core/src/types/dependency.rs | 2 + .../transformer/conversion/dependency_kind.rs | 4 + .../bundlers/default/src/DefaultBundler.js | 9 +- packages/core/core/src/types.js | 1 + packages/core/core/test/test-utils.js | 1 + packages/core/feature-flags/src/index.js | 1 + packages/core/feature-flags/src/types.js | 4 + packages/core/types-internal/src/index.js | 2 +- packages/examples/phases/.parcelrc | 3 + packages/examples/phases/package.json | 20 +++ packages/examples/phases/phases.d.ts | 15 ++ packages/examples/phases/src/index.html | 14 ++ packages/examples/phases/src/index.tsx | 28 +++ packages/examples/phases/src/lazy.tsx | 5 + 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/utils.tsx | 25 +++ packages/examples/phases/tsconfig.json | 8 + packages/packagers/js/package.json | 1 + .../packagers/js/src/ScopeHoistingPackager.js | 161 ++++++++++-------- packages/runtimes/js/src/JSRuntime.js | 15 +- .../js/src/helpers/browser/tier-loader.js | 23 +++ packages/transformers/js/core/src/collect.rs | 13 +- .../js/core/src/dependency_collector.rs | 100 ++++++++--- packages/transformers/js/core/src/hoist.rs | 32 +++- packages/transformers/js/core/src/lib.rs | 9 +- packages/transformers/js/core/src/utils.rs | 28 +++ packages/transformers/js/src/JSTransformer.js | 18 +- 29 files changed, 449 insertions(+), 108 deletions(-) create mode 100644 packages/examples/phases/.parcelrc create mode 100644 packages/examples/phases/package.json create mode 100644 packages/examples/phases/phases.d.ts create mode 100644 packages/examples/phases/src/index.html create mode 100644 packages/examples/phases/src/index.tsx create mode 100644 packages/examples/phases/src/lazy.tsx create mode 100644 packages/examples/phases/src/phase1.tsx create mode 100644 packages/examples/phases/src/phase2.tsx create mode 100644 packages/examples/phases/src/phase3.tsx create mode 100644 packages/examples/phases/src/utils.tsx create mode 100644 packages/examples/phases/tsconfig.json create mode 100644 packages/runtimes/js/src/helpers/browser/tier-loader.js diff --git a/crates/parcel_core/src/types/dependency.rs b/crates/parcel_core/src/types/dependency.rs index f850ea20c91..b8869e7d7cc 100644 --- a/crates/parcel_core/src/types/dependency.rs +++ b/crates/parcel_core/src/types/dependency.rs @@ -224,6 +224,8 @@ pub enum Priority { Parallel = 1, /// The dependency should be placed in a separate bundle that is loaded later Lazy = 2, + /// The dependency should be deferred to a different tier + Tier = 3, } impl Default for Priority { 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 feaa281168d..0ae50791ffd 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,6 +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, } } @@ -33,6 +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, } } diff --git a/packages/bundlers/default/src/DefaultBundler.js b/packages/bundlers/default/src/DefaultBundler.js index f5e0d4987c4..c60e9d9ffc6 100644 --- a/packages/bundlers/default/src/DefaultBundler.js +++ b/packages/bundlers/default/src/DefaultBundler.js @@ -95,6 +95,7 @@ const dependencyPriorityEdges = { sync: 1, parallel: 2, lazy: 3, + tier: 4, }; type DependencyBundleGraph = ContentGraph< @@ -277,7 +278,8 @@ function decorateLegacyGraph( ); for (let incomingDep of incomingDeps) { if ( - incomingDep.priority === 'lazy' && + (incomingDep.priority === 'lazy' || + incomingDep.priority === 'tier') && incomingDep.specifierType !== 'url' && bundle.hasDependency(incomingDep) ) { @@ -294,7 +296,7 @@ function decorateLegacyGraph( let incomingDeps = bundleGraph.getIncomingDependencies(manualSharedAsset); for (let incomingDep of incomingDeps) { if ( - incomingDep.priority === 'lazy' && + (incomingDep.priority === 'lazy' || incomingDep.priority === 'tier') && incomingDep.specifierType !== 'url' ) { let bundles = bundleGraph.getBundlesWithDependency(incomingDep); @@ -495,7 +497,7 @@ function createIdealGraph( if ( node.type === 'dependency' && - node.value.priority === 'lazy' && + (node.value.priority === 'lazy' || node.value.priority === 'tier') && parentAsset ) { // Don't walk past the bundle group assets @@ -584,6 +586,7 @@ 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) { diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index e165e24c57f..0df0a5d459b 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -114,6 +114,7 @@ export const Priority = { sync: 0, parallel: 1, lazy: 2, + tier: 3, }; // Must match package_json.rs in node-resolver-rs. diff --git a/packages/core/core/test/test-utils.js b/packages/core/core/test/test-utils.js index 16e50320b8f..dc930ab932f 100644 --- a/packages/core/core/test/test-utils.js +++ b/packages/core/core/test/test-utils.js @@ -57,6 +57,7 @@ export const DEFAULT_OPTIONS: ParcelOptions = { ...DEFAULT_FEATURE_FLAGS, exampleFeature: false, parcelV3: false, + tieredImports: false, importRetry: false, }, }; diff --git a/packages/core/feature-flags/src/index.js b/packages/core/feature-flags/src/index.js index 8d033d2e3ea..6ad935878d1 100644 --- a/packages/core/feature-flags/src/index.js +++ b/packages/core/feature-flags/src/index.js @@ -10,6 +10,7 @@ export const DEFAULT_FEATURE_FLAGS: FeatureFlags = { parcelV3: false, importRetry: false, ownedResolverStructures: false, + tieredImports: false, }; let featureFlagValues: FeatureFlags = {...DEFAULT_FEATURE_FLAGS}; diff --git a/packages/core/feature-flags/src/types.js b/packages/core/feature-flags/src/types.js index d2a8d774668..0863308c8b9 100644 --- a/packages/core/feature-flags/src/types.js +++ b/packages/core/feature-flags/src/types.js @@ -15,4 +15,8 @@ export type FeatureFlags = {| * Enable resolver refactor into owned data structures. */ ownedResolverStructures: boolean, + /** + * Tiered imports API + */ + +tieredImports: boolean, |}; diff --git a/packages/core/types-internal/src/index.js b/packages/core/types-internal/src/index.js index 782fc4c331a..a990ad44580 100644 --- a/packages/core/types-internal/src/index.js +++ b/packages/core/types-internal/src/index.js @@ -530,7 +530,7 @@ export interface MutableDependencySymbols // eslint-disable-next-line no-undef delete(exportSymbol: Symbol): void; } -export type DependencyPriority = 'sync' | 'parallel' | 'lazy'; +export type DependencyPriority = 'sync' | 'parallel' | 'lazy' | 'tier'; export type SpecifierType = 'commonjs' | 'esm' | 'url' | 'custom'; /** diff --git a/packages/examples/phases/.parcelrc b/packages/examples/phases/.parcelrc new file mode 100644 index 00000000000..47d1b5e3e88 --- /dev/null +++ b/packages/examples/phases/.parcelrc @@ -0,0 +1,3 @@ +{ + "extends": "@parcel/config-default" +} diff --git a/packages/examples/phases/package.json b/packages/examples/phases/package.json new file mode 100644 index 00000000000..74f3056df8c --- /dev/null +++ b/packages/examples/phases/package.json @@ -0,0 +1,20 @@ +{ + "name": "@parcel/phases-example", + "version": "2.12.0", + "license": "MIT", + "private": true, + "scripts": { + "start": "parcel serve src/index.html --no-cache --https --feature-flag tieredImports=true", + "start:prod": "yarn build && npx http-server dist/", + "build": "PARCEL_WORKERS=0 parcel build src/index.html --no-cache --feature-flag tieredImports=true", + "debug": "PARCEL_WORKERS=0 node --inspect-brk $(yarn bin parcel) serve src/index.html --no-cache --https --feature-flag tieredImports=true --no-hmr", + "debug:prod": "PARCEL_WORKERS=0 node --inspect-brk $(yarn bin parcel) build src/index.html --no-cache --feature-flag tieredImports=true" + }, + "devDependencies": { + "parcel": "2.12.0" + }, + "dependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2" + } +} diff --git a/packages/examples/phases/phases.d.ts b/packages/examples/phases/phases.d.ts new file mode 100644 index 00000000000..1101d1c470b --- /dev/null +++ b/packages/examples/phases/phases.d.ts @@ -0,0 +1,15 @@ +type ModuleRef<_> = string; +type ErrorMessage = 'You must annotate type with ""'; + +interface DeferredImport { + onReady(resource: () => void): () => void; + mod: T | null; +} + +declare function importDeferredForDisplay( + source: T extends void ? ErrorMessage : ModuleRef, +): DeferredImport; + +declare function importDeferred( + source: T extends void ? ErrorMessage : ModuleRef, +): DeferredImport; diff --git a/packages/examples/phases/src/index.html b/packages/examples/phases/src/index.html new file mode 100644 index 00000000000..a7cd70b7c6f --- /dev/null +++ b/packages/examples/phases/src/index.html @@ -0,0 +1,14 @@ + + + + + + + Parcel | React Phases + + +
+ + + + diff --git a/packages/examples/phases/src/index.tsx b/packages/examples/phases/src/index.tsx new file mode 100644 index 00000000000..b439f694433 --- /dev/null +++ b/packages/examples/phases/src/index.tsx @@ -0,0 +1,28 @@ +import React, {FC, Suspense, useEffect} from 'react'; +import ReactDOM from 'react-dom'; + +import ModulePhase1 from './phase1'; +import {deferredLoadComponent} from './utils'; +const Phase2 = deferredLoadComponent( + importDeferredForDisplay('./phase2'), +); +const Phase3 = deferredLoadComponent( + importDeferred('./phase3'), +); + +function App() { + return ( + <> +
App
+ + Loading...}> + + + Loading...}> + + + + ); +} + +ReactDOM.render(, document.getElementById('app')); diff --git a/packages/examples/phases/src/lazy.tsx b/packages/examples/phases/src/lazy.tsx new file mode 100644 index 00000000000..dcb33628453 --- /dev/null +++ b/packages/examples/phases/src/lazy.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +const Lazy = () =>
Lazy
; + +export default Lazy; diff --git a/packages/examples/phases/src/phase1.tsx b/packages/examples/phases/src/phase1.tsx new file mode 100644 index 00000000000..09ccd65dfae --- /dev/null +++ b/packages/examples/phases/src/phase1.tsx @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000000..beccaa153f3 --- /dev/null +++ b/packages/examples/phases/src/phase2.tsx @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000000..874982b8523 --- /dev/null +++ b/packages/examples/phases/src/phase3.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +const ModulePhase3 = () =>
Phase 3
; + +export default ModulePhase3; diff --git a/packages/examples/phases/src/utils.tsx b/packages/examples/phases/src/utils.tsx new file mode 100644 index 00000000000..1afd2afab3a --- /dev/null +++ b/packages/examples/phases/src/utils.tsx @@ -0,0 +1,25 @@ +import React, {FC, useEffect} from 'react'; + +export function deferredLoadComponent( + Resource: DeferredImport, +): FC { + let loaded = false; + let cleanUp: undefined | (() => void); + return function WrappedComponent(props) { + useEffect(() => { + return () => { + cleanUp?.(); + }; + }, []); + if (loaded) { + return ; + } else { + throw new Promise(resolve => { + cleanUp = Resource.onReady(() => { + loaded = true; + resolve(Resource); + }); + }); + } + }; +} diff --git a/packages/examples/phases/tsconfig.json b/packages/examples/phases/tsconfig.json new file mode 100644 index 00000000000..c2c9ac7191d --- /dev/null +++ b/packages/examples/phases/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "typeRoots": ["phases.d.ts"], + "jsx": "react", + "lib": ["ESNext", "DOM"], + "esModuleInterop": true + } +} diff --git a/packages/packagers/js/package.json b/packages/packagers/js/package.json index 81033c16210..6ea2cc74850 100644 --- a/packages/packagers/js/package.json +++ b/packages/packagers/js/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@parcel/diagnostic": "2.12.0", + "@parcel/feature-flags": "2.12.0", "@parcel/plugin": "2.12.0", "@parcel/rust": "2.12.0", "@parcel/source-map": "^2.1.1", diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index 4f3157b7849..26b6457ca6a 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -36,6 +36,8 @@ import { } from './utils'; // General regex used to replace imports with the resolved code, references with resolutions, // and count the number of newlines in the file for source maps. +const REPLACEMENT_RE_TIERED = + /\n|import\s+"([0-9a-f]{16}:.+?)";|(?:\$[0-9a-f]{16}\$exports)|(?:\$[0-9a-f]{16}\$(?:import|importAsync|require|importDeferredForDisplay|importDeferred)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g; const REPLACEMENT_RE = /\n|import\s+"([0-9a-f]{16}:.+?)";|(?:\$[0-9a-f]{16}\$exports)|(?:\$[0-9a-f]{16}\$(?:import|importAsync|require)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g; @@ -536,92 +538,100 @@ export class ScopeHoistingPackager { // in a single regex so that we only do one pass over the whole code. let offset = 0; let columnStartIndex = 0; - code = code.replace(REPLACEMENT_RE, (m, d, i) => { - if (m === '\n') { - columnStartIndex = i + offset + 1; - lineCount++; - return '\n'; - } - - // If we matched an import, replace with the source code for the dependency. - if (d != null) { - let deps = depMap.get(d); - if (!deps) { - return m; + code = code.replace( + this.options.featureFlags.tieredImports + ? REPLACEMENT_RE_TIERED + : REPLACEMENT_RE, + (m, d, i) => { + if (m === '\n') { + columnStartIndex = i + offset + 1; + lineCount++; + return '\n'; } - let replacement = ''; - - // A single `${id}:${specifier}:esm` might have been resolved to multiple assets due to - // reexports. - for (let dep of deps) { - let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle); - let skipped = this.bundleGraph.isDependencySkipped(dep); - if (resolved && !skipped) { - // Hoist variable declarations for the referenced parcelRequire dependencies - // after the dependency is declared. This handles the case where the resulting asset - // is wrapped, but the dependency in this asset is not marked as wrapped. This means - // that it was imported/required at the top-level, so its side effects should run immediately. - let [res, lines] = this.getHoistedParcelRequires( - asset, + // If we matched an import, replace with the source code for the dependency. + if (d != null) { + let deps = depMap.get(d); + if (!deps) { + return m; + } + + let replacement = ''; + + // A single `${id}:${specifier}:esm` might have been resolved to multiple assets due to + // reexports. + for (let dep of deps) { + let resolved = this.bundleGraph.getResolvedAsset( dep, - resolved, + this.bundle, ); - let map; - if ( - this.bundle.hasAsset(resolved) && - !this.seenAssets.has(resolved.id) - ) { - // If this asset is wrapped, we need to hoist the code for the dependency - // outside our parcelRequire.register wrapper. This is safe because all - // assets referenced by this asset will also be wrapped. Otherwise, inline the - // asset content where the import statement was. - if (shouldWrap) { - depContent.push(this.visitAsset(resolved)); - } else { - let [depCode, depMap, depLines] = this.visitAsset(resolved); - res = depCode + '\n' + res; - lines += 1 + depLines; - map = depMap; + let skipped = this.bundleGraph.isDependencySkipped(dep); + if (resolved && !skipped) { + // Hoist variable declarations for the referenced parcelRequire dependencies + // after the dependency is declared. This handles the case where the resulting asset + // is wrapped, but the dependency in this asset is not marked as wrapped. This means + // that it was imported/required at the top-level, so its side effects should run immediately. + let [res, lines] = this.getHoistedParcelRequires( + asset, + dep, + resolved, + ); + let map; + if ( + this.bundle.hasAsset(resolved) && + !this.seenAssets.has(resolved.id) + ) { + // If this asset is wrapped, we need to hoist the code for the dependency + // outside our parcelRequire.register wrapper. This is safe because all + // assets referenced by this asset will also be wrapped. Otherwise, inline the + // asset content where the import statement was. + if (shouldWrap) { + depContent.push(this.visitAsset(resolved)); + } else { + let [depCode, depMap, depLines] = this.visitAsset(resolved); + res = depCode + '\n' + res; + lines += 1 + depLines; + map = depMap; + } } - } - // Push this asset's source mappings down by the number of lines in the dependency - // plus the number of hoisted parcelRequires. Then insert the source map for the dependency. - if (sourceMap) { - if (lines > 0) { - sourceMap.offsetLines(lineCount + 1, lines); - } + // Push this asset's source mappings down by the number of lines in the dependency + // plus the number of hoisted parcelRequires. Then insert the source map for the dependency. + if (sourceMap) { + if (lines > 0) { + sourceMap.offsetLines(lineCount + 1, lines); + } - if (map) { - sourceMap.addSourceMap(map, lineCount); + if (map) { + sourceMap.addSourceMap(map, lineCount); + } } - } - replacement += res; - lineCount += lines; + replacement += res; + lineCount += lines; + } } + return replacement; } - return replacement; - } - // If it wasn't a dependency, then it was an inline replacement (e.g. $id$import$foo -> $id$export$foo). - let replacement = replacements.get(m) ?? m; - if (sourceMap) { - // Offset the source map columns for this line if the replacement was a different length. - // This assumes that the match and replacement both do not contain any newlines. - let lengthDifference = replacement.length - m.length; - if (lengthDifference !== 0) { - sourceMap.offsetColumns( - lineCount + 1, - i + offset - columnStartIndex + m.length, - lengthDifference, - ); - offset += lengthDifference; + // If it wasn't a dependency, then it was an inline replacement (e.g. $id$import$foo -> $id$export$foo). + let replacement = replacements.get(m) ?? m; + if (sourceMap) { + // Offset the source map columns for this line if the replacement was a different length. + // This assumes that the match and replacement both do not contain any newlines. + let lengthDifference = replacement.length - m.length; + if (lengthDifference !== 0) { + sourceMap.offsetColumns( + lineCount + 1, + i + offset - columnStartIndex + m.length, + lengthDifference, + ); + offset += lengthDifference; + } } - } - return replacement; - }); + return replacement; + }, + ); } // If the asset is wrapped, we need to insert the dependency code outside the parcelRequire.register @@ -743,7 +753,10 @@ ${code} // 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.meta.promiseSymbol) { + if ( + (dep.priority === 'lazy' || dep.priority === 'tier') && + dep.meta.promiseSymbol + ) { let promiseSymbol = dep.meta.promiseSymbol; invariant(typeof promiseSymbol === 'string'); let symbol = this.getSymbolResolution(asset, resolved, '*', dep); diff --git a/packages/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js index f5058c1c542..327c261204a 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 === 'lazy' || dependency.priority === 'tier') && dependency.specifierType !== 'url' ) { asyncDependencies.push(dependency); @@ -475,6 +475,7 @@ function getLoaderRuntime({ loaderCode = `(${loaderCode})`; } + let needsTierPrelude = false; if (mainBundle.type === 'js') { let parcelRequire = bundle.env.shouldScopeHoist ? 'parcelRequire' @@ -482,6 +483,15 @@ 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; + } } if (needsEsmLoadPrelude && options.featureFlags.importRetry) { @@ -507,6 +517,9 @@ 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 new file mode 100644 index 00000000000..a0ee550bd9d --- /dev/null +++ b/packages/runtimes/js/src/helpers/browser/tier-loader.js @@ -0,0 +1,23 @@ +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/collect.rs b/packages/transformers/js/core/src/collect.rs index 1912b72f97e..aa31dc7fa97 100644 --- a/packages/transformers/js/core/src/collect.rs +++ b/packages/transformers/js/core/src/collect.rs @@ -20,6 +20,7 @@ use crate::utils::is_unresolved; use crate::utils::match_export_name; use crate::utils::match_export_name_ident; use crate::utils::match_import; +use crate::utils::match_import_tier; use crate::utils::match_member_expr; use crate::utils::match_property_name; use crate::utils::match_require; @@ -46,6 +47,7 @@ pub enum ImportKind { Require, Import, DynamicImport, + TierImport, } #[derive(Debug)] @@ -768,6 +770,15 @@ impl Visit for Collect { self.add_bailout(span, BailoutReason::NonStaticDynamicImport); } + if let Some(source) = match_import_tier(node, self.ignore_mark) { + self.wrapped_requires.insert(source.to_string()); + let span = match node { + Expr::Call(c) => c.span, + _ => unreachable!(), + }; + self.add_bailout(span, BailoutReason::NonTopLevelRequire); + } + match node { Expr::Ident(ident) => { // Bail if `module` or `exports` are accessed non-statically. @@ -979,7 +990,7 @@ impl Collect { ImportKind::Import => self .wrapped_requires .insert(format!("{}{}", src.clone(), "esm")), - ImportKind::DynamicImport | ImportKind::Require => { + ImportKind::DynamicImport | ImportKind::Require | ImportKind::TierImport => { self.wrapped_requires.insert(src.to_string()) } }; diff --git a/packages/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index 3d176f14949..bbf565facd9 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -51,6 +51,16 @@ pub enum DependencyKind { /// import('./dependency').then(({x}) => {/* ... */}); /// ``` DynamicImport, + /// Corresponds to a for display (tier) import statement + /// ```skip + /// const {x} = importDeferredForDisplay('./dependency'); + /// ``` + DeferredForDisplayTierImport, + /// Corresponds to a deferred (tier) import statement + /// ```skip + /// const {x} = importDeferred('./dependency'); + /// ``` + DeferredTierImport, /// Corresponds to CJS require statements /// ```skip /// const {x} = require('./dependency'); @@ -430,6 +440,12 @@ impl<'a> Fold for DependencyCollector<'a> { Callee::Import(_) => DependencyKind::DynamicImport, Callee::Expr(expr) => { match &**expr { + Ident(ident) if ident.sym.to_string().as_str() == "importDeferredForDisplay" => { + DependencyKind::DeferredForDisplayTierImport + } + Ident(ident) if ident.sym.to_string().as_str() == "importDeferred" => { + DependencyKind::DeferredTierImport + } Ident(ident) => { // Bail if defined in scope if !is_unresolved(&ident, self.unresolved_mark) { @@ -739,31 +755,71 @@ impl<'a> Fold for DependencyCollector<'a> { }; // Replace import() with require() - if kind == DependencyKind::DynamicImport { - let mut call = node; - if !self.config.scope_hoist && !self.config.standalone { - let name = match &self.config.source_type { - SourceType::Module => "require", - SourceType::Script => "__parcel__require__", - }; - call.callee = ast::Callee::Expr(Box::new(ast::Expr::Ident(ast::Ident::new( - name.into(), - DUMMY_SP, - )))); - } + match &kind { + DependencyKind::DynamicImport => { + let mut call = node; + if !self.config.scope_hoist && !self.config.standalone { + let name = match &self.config.source_type { + SourceType::Module => "require", + SourceType::Script => "__parcel__require__", + }; + call.callee = ast::Callee::Expr(Box::new(ast::Expr::Ident(ast::Ident::new( + name.into(), + DUMMY_SP, + )))); + } - // Drop import attributes - call.args.truncate(1); + // Drop import attributes + call.args.truncate(1); - // Track the returned require call to be replaced with a promise chain. - let rewritten_call = rewrite_require_specifier(call, self.unresolved_mark); - self.require_node = Some(rewritten_call.clone()); - rewritten_call - } else if kind == DependencyKind::Require { + // Track the returned require call to be replaced with a promise chain. + let rewritten_call = rewrite_require_specifier(call, self.unresolved_mark); + self.require_node = Some(rewritten_call.clone()); + rewritten_call + } + DependencyKind::Require => // Don't continue traversing so that the `require` isn't replaced with undefined - rewrite_require_specifier(node, self.unresolved_mark) - } else { - node.fold_children_with(self) + { + rewrite_require_specifier(node, self.unresolved_mark) + } + DependencyKind::DeferredForDisplayTierImport | DependencyKind::DeferredTierImport => { + if !self.config.tier_imports { + return node.fold_children_with(self); + } + let mut call = node; + if call.args.len() != 1 { + self.diagnostics.push(Diagnostic { + message: format!("{} requires 1 argument", kind), + code_highlights: Some(vec![CodeHighlight { + message: None, + loc: SourceLocation::from(self.source_map, call.span), + }]), + hints: None, + show_environment: false, + severity: DiagnosticSeverity::Error, + documentation_url: None, + }); + } + + // Convert to require without scope hoisting + if !self.config.scope_hoist && !self.config.standalone { + let name = match &self.config.source_type { + SourceType::Module => "require", + SourceType::Script => "__parcel__require__", + }; + call.callee = ast::Callee::Expr(Box::new(ast::Expr::Ident(ast::Ident::new( + name.into(), + DUMMY_SP, + )))); + } + + // Track the returned require call to be replaced with a promise chain. + let rewritten_call = rewrite_require_specifier(call, self.unresolved_mark); + self.require_node = Some(rewritten_call.clone()); + + rewritten_call + } + _ => node.fold_children_with(self), } } diff --git a/packages/transformers/js/core/src/hoist.rs b/packages/transformers/js/core/src/hoist.rs index 27ea16cae5e..bd59c72455c 100644 --- a/packages/transformers/js/core/src/hoist.rs +++ b/packages/transformers/js/core/src/hoist.rs @@ -27,6 +27,7 @@ use crate::utils::is_unresolved; use crate::utils::match_export_name; use crate::utils::match_export_name_ident; use crate::utils::match_import; +use crate::utils::match_import_tier; use crate::utils::match_member_expr; use crate::utils::match_property_name; use crate::utils::match_require; @@ -48,8 +49,9 @@ pub fn hoist( module_id: &str, unresolved_mark: Mark, collect: &Collect, + tiered_imports: bool, ) -> Result<(Module, HoistResult, Vec), Vec> { - let mut hoist = Hoist::new(module_id, unresolved_mark, collect); + let mut hoist = Hoist::new(module_id, unresolved_mark, collect, tiered_imports); let module = module.fold_with(&mut hoist); if !hoist.diagnostics.is_empty() { @@ -128,6 +130,7 @@ struct Hoist<'a> { in_function_scope: bool, diagnostics: Vec, unresolved_mark: Mark, + tiered_imports: bool, } /// Data pertaining to mangled identifiers replacing import and export statements @@ -336,7 +339,12 @@ pub struct HoistResult { } impl<'a> Hoist<'a> { - fn new(module_id: &'a str, unresolved_mark: Mark, collect: &'a Collect) -> Self { + fn new( + module_id: &'a str, + unresolved_mark: Mark, + collect: &'a Collect, + tiered_imports: bool, + ) -> Self { Hoist { module_id, collect, @@ -351,6 +359,7 @@ impl<'a> Hoist<'a> { in_function_scope: false, diagnostics: vec![], unresolved_mark, + tiered_imports, } } @@ -952,6 +961,19 @@ impl<'a> Fold for Hoist<'a> { } return Expr::Ident(Ident::new(name, call.span)); } + + if self.tiered_imports { + if let Some(source) = match_import_tier(&node, self.collect.ignore_mark) { + self.add_require(&source, ImportKind::TierImport); + return Expr::Ident(self.get_import_ident( + call.span, + &source, + &("*".into()), + SourceLocation::from(&self.collect.source_map, call.span), + ImportKind::TierImport, + )); + } + } } Expr::This(this) => { if !self.in_function_scope { @@ -1236,7 +1258,9 @@ impl<'a> Hoist<'a> { fn add_require(&mut self, source: &JsWord, import_kind: ImportKind) { let src = match import_kind { ImportKind::Import => format!("{}:{}:{}", self.module_id, source, "esm"), - ImportKind::DynamicImport | ImportKind::Require => format!("{}:{}", self.module_id, source), + ImportKind::DynamicImport | ImportKind::Require | ImportKind::TierImport => { + format!("{}:{}", self.module_id, source) + } }; self .module_items @@ -1420,7 +1444,7 @@ mod tests { module.visit_with(&mut collect); let (module, res) = { - let mut hoist = Hoist::new("abc", unresolved_mark, &collect); + let mut hoist = Hoist::new("abc", unresolved_mark, &collect, false); let module = module.fold_with(&mut hoist); (module, hoist.get_result()) }; diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index d6b0fbd4677..f2dfc19319f 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -127,6 +127,7 @@ pub struct Config { pub is_swc_helpers: bool, pub standalone: bool, pub inline_constants: bool, + pub tier_imports: bool, } #[derive(Serialize, Debug, Default)] @@ -494,7 +495,13 @@ pub fn transform( } let module = if config.scope_hoist { - let res = hoist(module, config.module_id.as_str(), unresolved_mark, &collect); + let res = hoist( + module, + config.module_id.as_str(), + unresolved_mark, + &collect, + config.tier_imports, + ); match res { Ok((module, hoist_result, hoist_diagnostics)) => { result.hoist_result = Some(hoist_result); diff --git a/packages/transformers/js/core/src/utils.rs b/packages/transformers/js/core/src/utils.rs index a60628e7ea7..1097f094bc2 100644 --- a/packages/transformers/js/core/src/utils.rs +++ b/packages/transformers/js/core/src/utils.rs @@ -168,6 +168,34 @@ pub fn match_require(node: &ast::Expr, unresolved_mark: Mark, ignore_mark: Mark) } } +/// This matches an expression like `importDeferredForDisplay('id')` or `importDeferred('id')` and returns the dependency id. +pub fn match_import_tier(node: &ast::Expr, ignore_mark: Mark) -> Option { + use ast::*; + + match node { + Expr::Call(call) => match &call.callee { + Callee::Expr(expr) => match &**expr { + Expr::Ident(ident) => { + if ident.sym == js_word!("importDeferredForDisplay") + || ident.sym == js_word!("importDeferred") + { + if !is_marked(ident.span, ignore_mark) && call.args.len() == 1 { + if let Some(arg) = call.args.first() { + return match_str(&arg.expr).map(|(name, _)| name); + } + } + return None; + } + None + } + _ => None, + }, + _ => None, + }, + _ => None, + } +} + pub fn match_import(node: &ast::Expr, ignore_mark: Mark) -> Option { use ast::*; diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index 5db4ac97947..945ea2ebda1 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -574,6 +574,7 @@ export default (new Transformer({ } } : null, + tier_imports: options.featureFlags.tieredImports, }); if (is_constant_module) { @@ -846,11 +847,25 @@ export default (new Transformer({ range = pkg.dependencies[module]; } + let priority; + switch (dep.kind) { + case 'DeferredForDisplayTierImport': + case 'DeferredTierImport': + priority = 'tier'; + break; + case 'DynamicImport': + priority = 'lazy'; + break; + default: + priority = 'sync'; + break; + } + asset.addDependency({ specifier: dep.specifier, specifierType: dep.kind === 'Require' ? 'commonjs' : 'esm', loc: convertLoc(dep.loc), - priority: dep.kind === 'DynamicImport' ? 'lazy' : 'sync', + priority, isOptional: dep.is_optional, meta, resolveFrom: isHelper ? __filename : undefined, @@ -882,6 +897,7 @@ export default (new Transformer({ .getDependencies() .map(dep => [dep.meta.placeholder ?? dep.specifier, dep]), ); + for (let dep of deps.values()) { dep.symbols.ensure(); } From 5403fc9555ce09b957f5578049b929db30cd1f77 Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Tue, 30 Jul 2024 10:55:20 +1000 Subject: [PATCH 02/10] 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 | 3 ++ 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(+), 120 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 0863308c8b9..a3578215cdd 100644 --- a/packages/core/feature-flags/src/types.js +++ b/packages/core/feature-flags/src/types.js @@ -17,6 +17,9 @@ export type FeatureFlags = {| ownedResolverStructures: 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 327c261204a..f5058c1c542 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; - } } if (needsEsmLoadPrelude && options.featureFlags.importRetry) { @@ -517,9 +507,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 bbf565facd9..8a527fae7d1 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -51,16 +51,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'); @@ -441,10 +441,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 @@ -782,7 +782,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': From d4e043be82ee8385b6e85b7c2369b06f9e1ac27d Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Thu, 1 Aug 2024 17:29:19 +1000 Subject: [PATCH 03/10] 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 | 14 +++++ packages/transformers/js/src/tier-helpers.js | 23 +++++++ 9 files changed, 166 insertions(+), 86 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 8a527fae7d1..102ac41d31c 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 path_slash::PathBufExt; use serde::Deserialize; @@ -19,6 +21,7 @@ use swc_core::ecma::ast::{self}; use swc_core::ecma::atoms::js_word; use swc_core::ecma::atoms::JsWord; use swc_core::ecma::utils::stack_size::maybe_grow_default; +use swc_core::ecma::utils::ExprFactory; use swc_core::ecma::visit::Fold; use swc_core::ecma::visit::FoldWith; @@ -133,14 +136,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: Lrc, 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, @@ -151,7 +155,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> { @@ -165,6 +174,7 @@ struct DependencyCollector<'a> { config: &'a Config, diagnostics: &'a mut Vec, import_meta: Option, + needs_tier_helpers: bool, } impl<'a> DependencyCollector<'a> { @@ -331,6 +341,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 } @@ -801,6 +832,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 { @@ -817,7 +851,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 f2dfc19319f..8c6d197e7c4 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.clone(), - &mut result.dependencies, - ignore_mark, - unresolved_mark, - &config, - &mut diagnostics, - ), + let (module, needs_tier_helpers) = dependency_collector( + module, + source_map.clone(), + &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..ff9487721e2 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -415,6 +415,7 @@ export default (new Transformer({ hoist_result, symbol_result, needs_esm_helpers, + needs_tier_helpers, diagnostics, used_env, has_node_replacements, @@ -1070,6 +1071,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); + } + }, + }; +} From 7c76b0c433547653a6a856140d3f925c840bd57a Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Mon, 5 Aug 2024 14:23:36 +1000 Subject: [PATCH 04/10] Update examples for memoisation and move back to packager --- packages/examples/phases/package.json | 2 +- packages/examples/phases/src/utils.tsx | 36 ++++++---- .../packagers/js/src/ScopeHoistingPackager.js | 23 +++++-- packages/packagers/js/src/dev-prelude.js | 23 +++++++ packages/packagers/js/src/helpers.js | 27 ++++++++ .../js/core/src/dependency_collector.rs | 66 ++++++------------- packages/transformers/js/core/src/lib.rs | 20 +++--- packages/transformers/js/src/JSTransformer.js | 14 ---- packages/transformers/js/src/tier-helpers.js | 23 ------- 9 files changed, 121 insertions(+), 113 deletions(-) delete mode 100644 packages/transformers/js/src/tier-helpers.js diff --git a/packages/examples/phases/package.json b/packages/examples/phases/package.json index 74f3056df8c..7fd42eec7ae 100644 --- a/packages/examples/phases/package.json +++ b/packages/examples/phases/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "start": "parcel serve src/index.html --no-cache --https --feature-flag tieredImports=true", - "start:prod": "yarn build && npx http-server dist/", + "start:prod": "yarn build && npx http-server -p 1234 dist/", "build": "PARCEL_WORKERS=0 parcel build src/index.html --no-cache --feature-flag tieredImports=true", "debug": "PARCEL_WORKERS=0 node --inspect-brk $(yarn bin parcel) serve src/index.html --no-cache --https --feature-flag tieredImports=true --no-hmr", "debug:prod": "PARCEL_WORKERS=0 node --inspect-brk $(yarn bin parcel) build src/index.html --no-cache --feature-flag tieredImports=true" diff --git a/packages/examples/phases/src/utils.tsx b/packages/examples/phases/src/utils.tsx index 625f04b7a47..e125e8e7637 100644 --- a/packages/examples/phases/src/utils.tsx +++ b/packages/examples/phases/src/utils.tsx @@ -1,15 +1,21 @@ import React, { ComponentType, ForwardRefExoticComponent, + ForwardedRef, + MemoExoticComponent, + PropsWithChildren, PropsWithoutRef, RefAttributes, forwardRef, + memo, } from 'react'; export function deferredLoadComponent

( resource: DeferredImport<{default: ComponentType

}>, -): ForwardRefExoticComponent< - PropsWithoutRef

& RefAttributes> +): MemoExoticComponent< + ForwardRefExoticComponent< + PropsWithoutRef

& RefAttributes> + > > { // Create a deferred component map in the global context, so we can reuse the components everywhere if (!globalThis.deferredComponentMap) { @@ -21,25 +27,31 @@ export function deferredLoadComponent

( } let Component: ComponentType | undefined; - const loader = new Promise(resolve => { + let loader = new Promise(resolve => { resource.onReady(loaded => { Component = loaded; resolve(loaded); }); }); - const wrapper = forwardRef, P>(function DeferredComponent( - props, - ref, + const wrapper = function DeferredComponent( + props: PropsWithChildren

, + ref: ForwardedRef>, ) { if (Component) { return ; - } else { - throw loader; } - }); - // Store in weakmap so we only have one instance - globalThis.deferredComponentMap.set(resource, wrapper); - return wrapper; + throw loader; + }; + + // Support refs in the deferred component + const forwardedRef = forwardRef(wrapper); + + // Memoise so we avoid re-renders + const memoised = memo(forwardedRef); + + // Store in weak map so we only have one instance + globalThis.deferredComponentMap.set(resource, memoised); + return memoised; } diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index b7f129b16f3..0b1bc0d6a93 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -741,13 +741,22 @@ ${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. diff --git a/packages/packagers/js/src/dev-prelude.js b/packages/packagers/js/src/dev-prelude.js index a44249cef3d..cc5b8a5f84c 100644 --- a/packages/packagers/js/src/dev-prelude.js +++ b/packages/packagers/js/src/dev-prelude.js @@ -109,6 +109,29 @@ {}, ]; }; + newRequire.tier = function (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); + } + }, + }; + }; Object.defineProperty(newRequire, 'root', { get: function () { diff --git a/packages/packagers/js/src/helpers.js b/packages/packagers/js/src/helpers.js index de455c32c57..6144d9cbee3 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/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index 102ac41d31c..7782ab1d885 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -5,7 +5,6 @@ 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 path_slash::PathBufExt; use serde::Deserialize; @@ -136,15 +135,14 @@ 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: Lrc, items: &'a mut Vec, ignore_mark: swc_core::common::Mark, unresolved_mark: swc_core::common::Mark, config: &'a Config, diagnostics: &'a mut Vec, -) -> (Module, bool) { - let mut fold = DependencyCollector { +) -> impl Fold + 'a { + DependencyCollector { source_map, items, in_try: false, @@ -155,12 +153,7 @@ 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> { @@ -174,7 +167,6 @@ struct DependencyCollector<'a> { config: &'a Config, diagnostics: &'a mut Vec, import_meta: Option, - needs_tier_helpers: bool, } impl<'a> DependencyCollector<'a> { @@ -342,26 +334,6 @@ impl<'a> Fold for DependencyCollector<'a> { ); } - 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 } @@ -832,9 +804,6 @@ 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 { @@ -851,19 +820,24 @@ impl<'a> Fold for DependencyCollector<'a> { let rewritten_call = rewrite_require_specifier(call, self.unresolved_mark); self.require_node = Some(rewritten_call.clone()); - // 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)), + if !self.config.scope_hoist && !self.config.standalone { + // Wrap with parcel tiers when not scope hoisting + 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(Member(MemberExpr { + obj: Box::new(ast::Expr::Ident(ast::Ident::new("module".into(), DUMMY_SP))), + prop: MemberProp::Ident(ast::Ident::new("bundle".into(), DUMMY_SP)), + span: DUMMY_SP, + })), + prop: MemberProp::Ident(ast::Ident::new("tier".into(), DUMMY_SP)), + span: DUMMY_SP, + }))), + } + } else { + rewritten_call } } _ => node.fold_children_with(self), diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 8c6d197e7c4..f2dfc19319f 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -145,7 +145,6 @@ 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 { @@ -460,16 +459,17 @@ pub fn transform( }; let ignore_mark = Mark::fresh(Mark::root()); - let (module, needs_tier_helpers) = dependency_collector( - module, - source_map.clone(), - &mut result.dependencies, - ignore_mark, - unresolved_mark, - &config, - &mut diagnostics, + let module = module.fold_with( + // Collect dependencies + &mut dependency_collector( + source_map.clone(), + &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 ff9487721e2..611e25cb48c 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -415,7 +415,6 @@ export default (new Transformer({ hoist_result, symbol_result, needs_esm_helpers, - needs_tier_helpers, diagnostics, used_env, has_node_replacements, @@ -1071,19 +1070,6 @@ 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 deleted file mode 100644 index b9e1a944575..00000000000 --- a/packages/transformers/js/src/tier-helpers.js +++ /dev/null @@ -1,23 +0,0 @@ -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); - } - }, - }; -} From bfd803b6eb6409b1174c78b71fe3d929340991bb Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Mon, 5 Aug 2024 15:57:24 +1000 Subject: [PATCH 05/10] Add tests for dependency kind --- .../transformer/conversion/dependency_kind.rs | 42 ++++++++++++++++++- .../src/transformer/test_helpers.rs | 6 +++ .../js/core/src/dependency_collector.rs | 11 +++-- 3 files changed, 54 insertions(+), 5 deletions(-) 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 c4b02543dbf..e8154bc0f4d 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 @@ -42,8 +42,10 @@ pub(crate) fn convert_specifier_type( #[cfg(test)] mod test { - use crate::transformer::test_helpers::run_swc_core_transform; - use parcel_js_swc_core::DependencyKind; + use crate::transformer::test_helpers::{ + make_test_swc_config, run_swc_core_transform, run_swc_core_transform_with_config, + }; + use parcel_js_swc_core::{Config, DependencyKind}; use super::*; @@ -59,6 +61,36 @@ mod test { assert_eq!(convert_specifier_type(&dependency), SpecifierType::Esm); } + #[test] + fn test_deferred_for_display_dependency_kind() { + let dependency = get_last_dependency_with_config(Config { + tier_imports: true, + ..make_test_swc_config( + r#" + const x = importDeferredForDisplay('other'); + "#, + ) + }); + assert_eq!(dependency.kind, DependencyKind::DeferredForDisplayImport); + assert_eq!(convert_priority(&dependency), Priority::Tier); + assert_eq!(convert_specifier_type(&dependency), SpecifierType::Esm); + } + + #[test] + fn test_deferred_dependency_kind() { + let dependency = get_last_dependency_with_config(Config { + tier_imports: true, + ..make_test_swc_config( + r#" + const x = importDeferred('other'); + "#, + ) + }); + assert_eq!(dependency.kind, DependencyKind::DeferredImport); + assert_eq!(convert_priority(&dependency), Priority::Tier); + assert_eq!(convert_specifier_type(&dependency), SpecifierType::Esm); + } + #[test] fn test_dynamic_import_dependency_kind() { let dependency = get_last_dependency( @@ -171,4 +203,10 @@ mod test { let swc_output = run_swc_core_transform(source); swc_output.dependencies.last().unwrap().clone() } + + /// Run the SWC transformer with a config and return the last dependency descriptor listed + fn get_last_dependency_with_config(config: Config) -> parcel_js_swc_core::DependencyDescriptor { + let swc_output = run_swc_core_transform_with_config(config); + swc_output.dependencies.last().unwrap().clone() + } } diff --git a/crates/parcel_plugin_transformer_js/src/transformer/test_helpers.rs b/crates/parcel_plugin_transformer_js/src/transformer/test_helpers.rs index c89c1518bf8..7b3da009ff3 100644 --- a/crates/parcel_plugin_transformer_js/src/transformer/test_helpers.rs +++ b/crates/parcel_plugin_transformer_js/src/transformer/test_helpers.rs @@ -6,6 +6,12 @@ pub(crate) fn run_swc_core_transform(source: &str) -> TransformResult { swc_output } +/// Parse a file with the `parcel_js_swc_core` parser for testing and specify a config +pub(crate) fn run_swc_core_transform_with_config(config: Config) -> TransformResult { + let swc_output = parcel_js_swc_core::transform(config, None).unwrap(); + swc_output +} + /// SWC configuration for testing pub(crate) fn make_test_swc_config(source: &str) -> Config { Config { diff --git a/packages/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index 7782ab1d885..f2a84adbb23 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -443,10 +443,15 @@ impl<'a> Fold for DependencyCollector<'a> { Callee::Import(_) => DependencyKind::DynamicImport, Callee::Expr(expr) => { match &**expr { - Ident(ident) if ident.sym.to_string().as_str() == "importDeferredForDisplay" => { + Ident(ident) + if self.config.tier_imports + && ident.sym.to_string().as_str() == "importDeferredForDisplay" => + { DependencyKind::DeferredForDisplayImport } - Ident(ident) if ident.sym.to_string().as_str() == "importDeferred" => { + Ident(ident) + if self.config.tier_imports && ident.sym.to_string().as_str() == "importDeferred" => + { DependencyKind::DeferredImport } Ident(ident) => { @@ -795,7 +800,7 @@ impl<'a> Fold for DependencyCollector<'a> { message: format!("{} requires 1 argument", kind), code_highlights: Some(vec![CodeHighlight { message: None, - loc: SourceLocation::from(self.source_map, call.span), + loc: SourceLocation::from(&self.source_map, call.span), }]), hints: None, show_environment: false, From 7a18e5ddda62c3ace3028891e94028fe7c2acd0a Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Mon, 5 Aug 2024 16:30:00 +1000 Subject: [PATCH 06/10] Remove modern syntax from packager --- packages/packagers/js/src/dev-prelude.js | 10 +++++----- packages/packagers/js/src/helpers.js | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/packagers/js/src/dev-prelude.js b/packages/packagers/js/src/dev-prelude.js index cc5b8a5f84c..f6de693a470 100644 --- a/packages/packagers/js/src/dev-prelude.js +++ b/packages/packagers/js/src/dev-prelude.js @@ -113,17 +113,17 @@ var listeners = new Set(); var resolved = false; if (loader instanceof Promise) { - loader.then(mod => { + loader.then(function (mod) { resolved = true; - for (let listener of listeners) { - listener?.(mod.default); - } + listeners.forEach(function (listener) { + if (listener) listener(mod.default); + }); }); } else { resolved = true; } return { - onReady: listener => { + onReady: function (listener) { if (resolved) { listener(loader.default); } else { diff --git a/packages/packagers/js/src/helpers.js b/packages/packagers/js/src/helpers.js index 6144d9cbee3..00c66f96e17 100644 --- a/packages/packagers/js/src/helpers.js +++ b/packages/packagers/js/src/helpers.js @@ -165,17 +165,17 @@ function $parcel$tier(loader) { var listeners = new Set(); var resolved = false; if (loader instanceof Promise) { - loader.then(mod => { + loader.then(function (mod) { resolved = true; - for (let listener of listeners) { - listener?.(mod.default); - } + listeners.forEach(function (listener) { + if (listener) listener(mod.default); + }); }); } else { resolved = true; } return { - onReady: listener => { + onReady: function (listener) { if (resolved) { listener(loader.default); } else { From 4ad1af2df91280af6822c6cc3fb7ce48eedff57a Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Mon, 5 Aug 2024 17:52:19 +1000 Subject: [PATCH 07/10] Fix test referencing old symbol --- packages/core/integration-tests/test/sourcemaps.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/integration-tests/test/sourcemaps.js b/packages/core/integration-tests/test/sourcemaps.js index a22caf8b57d..e700ef423d7 100644 --- a/packages/core/integration-tests/test/sourcemaps.js +++ b/packages/core/integration-tests/test/sourcemaps.js @@ -468,7 +468,7 @@ describe.v2('sourcemaps', function () { source: inputs[1], generated: raw, str: 'exports.a', - generatedStr: 'o.a', + generatedStr: 't.a', sourcePath: 'local.js', }); @@ -477,7 +477,7 @@ describe.v2('sourcemaps', function () { source: inputs[2], generated: raw, str: 'exports.count = function(a, b) {', - generatedStr: 'o.count=function(e,n){', + generatedStr: 't.count=function(e,n){', sourcePath: 'utils/util.js', }); }); From f6a1411d98bef0fec17d92698afac1d57e944924 Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Tue, 6 Aug 2024 15:22:49 +1000 Subject: [PATCH 08/10] Use if instead of match to simplify tier match --- packages/transformers/js/core/src/utils.rs | 30 +++++++++------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/transformers/js/core/src/utils.rs b/packages/transformers/js/core/src/utils.rs index 1097f094bc2..a31d3692fc5 100644 --- a/packages/transformers/js/core/src/utils.rs +++ b/packages/transformers/js/core/src/utils.rs @@ -172,28 +172,22 @@ pub fn match_require(node: &ast::Expr, unresolved_mark: Mark, ignore_mark: Mark) pub fn match_import_tier(node: &ast::Expr, ignore_mark: Mark) -> Option { use ast::*; - match node { - Expr::Call(call) => match &call.callee { - Callee::Expr(expr) => match &**expr { - Expr::Ident(ident) => { - if ident.sym == js_word!("importDeferredForDisplay") - || ident.sym == js_word!("importDeferred") - { - if !is_marked(ident.span, ignore_mark) && call.args.len() == 1 { - if let Some(arg) = call.args.first() { - return match_str(&arg.expr).map(|(name, _)| name); - } + if let Expr::Call(call) = node { + if let Callee::Expr(expr) = &call.callee { + if let Expr::Ident(ident) = &**expr { + if ident.sym == js_word!("importDeferredForDisplay") + || ident.sym == js_word!("importDeferred") + { + if !is_marked(ident.span, ignore_mark) && call.args.len() == 1 { + if let Some(arg) = call.args.first() { + match_str(&arg.expr).map(|(name, _)| name); } - return None; } - None } - _ => None, - }, - _ => None, - }, - _ => None, + } + } } + None } pub fn match_import(node: &ast::Expr, ignore_mark: Mark) -> Option { From 0f1aecacc62ce8269354c6303e6c7f48218860ec Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Wed, 7 Aug 2024 09:06:42 +1000 Subject: [PATCH 09/10] Fix not returning string match --- packages/examples/phases/package.json | 4 ++-- packages/transformers/js/core/src/utils.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/examples/phases/package.json b/packages/examples/phases/package.json index 7fd42eec7ae..c54caccb7d6 100644 --- a/packages/examples/phases/package.json +++ b/packages/examples/phases/package.json @@ -4,8 +4,8 @@ "license": "MIT", "private": true, "scripts": { - "start": "parcel serve src/index.html --no-cache --https --feature-flag tieredImports=true", - "start:prod": "yarn build && npx http-server -p 1234 dist/", + "start": "parcel serve src/index.html --no-cache --no-hmr --https --feature-flag tieredImports=true", + "start:prod": "yarn build && npx http-server -p 1234 --ssl --key ../.parcel-cache/private.pem --cert ../.parcel-cache/primary.crt dist/", "build": "PARCEL_WORKERS=0 parcel build src/index.html --no-cache --feature-flag tieredImports=true", "debug": "PARCEL_WORKERS=0 node --inspect-brk $(yarn bin parcel) serve src/index.html --no-cache --https --feature-flag tieredImports=true --no-hmr", "debug:prod": "PARCEL_WORKERS=0 node --inspect-brk $(yarn bin parcel) build src/index.html --no-cache --feature-flag tieredImports=true" diff --git a/packages/transformers/js/core/src/utils.rs b/packages/transformers/js/core/src/utils.rs index a31d3692fc5..82420e22c60 100644 --- a/packages/transformers/js/core/src/utils.rs +++ b/packages/transformers/js/core/src/utils.rs @@ -180,7 +180,7 @@ pub fn match_import_tier(node: &ast::Expr, ignore_mark: Mark) -> Option { if !is_marked(ident.span, ignore_mark) && call.args.len() == 1 { if let Some(arg) = call.args.first() { - match_str(&arg.expr).map(|(name, _)| name); + return match_str(&arg.expr).map(|(name, _)| name); } } } From ca5b78b6267364cdb7640c7706bdb1826c9ce485 Mon Sep 17 00:00:00 2001 From: Jake Lane Date: Wed, 7 Aug 2024 09:43:10 +1000 Subject: [PATCH 10/10] Move to unsafe_ API --- .../src/transformer/conversion/dependency_kind.rs | 4 ++-- packages/examples/phases/phases.d.ts | 4 ++-- packages/examples/phases/src/index.tsx | 5 +++-- packages/packagers/js/src/ScopeHoistingPackager.js | 2 +- .../transformers/js/core/src/dependency_collector.rs | 9 +++++---- packages/transformers/js/core/src/utils.rs | 6 +++--- 6 files changed, 16 insertions(+), 14 deletions(-) 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 e8154bc0f4d..cb0899b5f71 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 @@ -67,7 +67,7 @@ mod test { tier_imports: true, ..make_test_swc_config( r#" - const x = importDeferredForDisplay('other'); + const x = unsafe_importDeferredForDisplay('other'); "#, ) }); @@ -82,7 +82,7 @@ mod test { tier_imports: true, ..make_test_swc_config( r#" - const x = importDeferred('other'); + const x = unsafe_importDeferred('other'); "#, ) }); diff --git a/packages/examples/phases/phases.d.ts b/packages/examples/phases/phases.d.ts index c914e1855df..a48ca721a1e 100644 --- a/packages/examples/phases/phases.d.ts +++ b/packages/examples/phases/phases.d.ts @@ -5,10 +5,10 @@ interface DeferredImport { onReady(resource: (mod: T['default']) => void): void; } -declare function importDeferredForDisplay( +declare function unsafe_importDeferredForDisplay( source: T extends void ? ErrorMessage : ModuleRef, ): DeferredImport; -declare function importDeferred( +declare function unsafe_importDeferred( source: T extends void ? ErrorMessage : ModuleRef, ): DeferredImport; diff --git a/packages/examples/phases/src/index.tsx b/packages/examples/phases/src/index.tsx index b08a0ac1db6..1f6cc4338d1 100644 --- a/packages/examples/phases/src/index.tsx +++ b/packages/examples/phases/src/index.tsx @@ -3,8 +3,9 @@ import ReactDOM from 'react-dom'; import Tier1 from './tier1'; const DeferredTier2 = - importDeferredForDisplay('./tier2'); -const DeferredTier3 = importDeferred('./tier3'); + unsafe_importDeferredForDisplay('./tier2'); +const DeferredTier3 = + unsafe_importDeferred('./tier3'); import {deferredLoadComponent} from './utils'; diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index 0b1bc0d6a93..1e30f38322f 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -37,7 +37,7 @@ import { // General regex used to replace imports with the resolved code, references with resolutions, // and count the number of newlines in the file for source maps. const REPLACEMENT_RE_TIERED = - /\n|import\s+"([0-9a-f]{16}:.+?)";|(?:\$[0-9a-f]{16}\$exports)|(?:\$[0-9a-f]{16}\$(?:import|importAsync|require|importDeferredForDisplay|importDeferred)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g; + /\n|import\s+"([0-9a-f]{16}:.+?)";|(?:\$[0-9a-f]{16}\$exports)|(?:\$[0-9a-f]{16}\$(?:import|importAsync|require|unsafe_importDeferredForDisplay|unsafe_importDeferred)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g; const REPLACEMENT_RE = /\n|import\s+"([0-9a-f]{16}:.+?)";|(?:\$[0-9a-f]{16}\$exports)|(?:\$[0-9a-f]{16}\$(?:import|importAsync|require)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g; diff --git a/packages/transformers/js/core/src/dependency_collector.rs b/packages/transformers/js/core/src/dependency_collector.rs index f2a84adbb23..7668808df87 100644 --- a/packages/transformers/js/core/src/dependency_collector.rs +++ b/packages/transformers/js/core/src/dependency_collector.rs @@ -55,12 +55,12 @@ pub enum DependencyKind { DynamicImport, /// Corresponds to a deferred for display import statement /// ```skip - /// const DependencyDeferredForDisplay = importDeferredForDisplay('./dependency'); + /// const DependencyDeferredForDisplay = unsafe_importDeferredForDisplay('./dependency'); /// ``` DeferredForDisplayImport, /// Corresponds to a deferred import statement /// ```skip - /// const DependencyDeferred = importDeferred('./dependency'); + /// const DependencyDeferred = unsafe_importDeferred('./dependency'); /// ``` DeferredImport, /// Corresponds to CJS require statements @@ -445,12 +445,13 @@ impl<'a> Fold for DependencyCollector<'a> { match &**expr { Ident(ident) if self.config.tier_imports - && ident.sym.to_string().as_str() == "importDeferredForDisplay" => + && ident.sym.to_string().as_str() == "unsafe_importDeferredForDisplay" => { DependencyKind::DeferredForDisplayImport } Ident(ident) - if self.config.tier_imports && ident.sym.to_string().as_str() == "importDeferred" => + if self.config.tier_imports + && ident.sym.to_string().as_str() == "unsafe_importDeferred" => { DependencyKind::DeferredImport } diff --git a/packages/transformers/js/core/src/utils.rs b/packages/transformers/js/core/src/utils.rs index 82420e22c60..2f8bda92ab9 100644 --- a/packages/transformers/js/core/src/utils.rs +++ b/packages/transformers/js/core/src/utils.rs @@ -168,15 +168,15 @@ pub fn match_require(node: &ast::Expr, unresolved_mark: Mark, ignore_mark: Mark) } } -/// This matches an expression like `importDeferredForDisplay('id')` or `importDeferred('id')` and returns the dependency id. +/// This matches an expression like `unsafe_importDeferredForDisplay('id')` or `unsafe_importDeferred('id')` and returns the dependency id. pub fn match_import_tier(node: &ast::Expr, ignore_mark: Mark) -> Option { use ast::*; if let Expr::Call(call) = node { if let Callee::Expr(expr) = &call.callee { if let Expr::Ident(ident) = &**expr { - if ident.sym == js_word!("importDeferredForDisplay") - || ident.sym == js_word!("importDeferred") + if ident.sym == js_word!("unsafe_importDeferredForDisplay") + || ident.sym == js_word!("unsafe_importDeferred") { if !is_marked(ident.span, ignore_mark) && call.args.len() == 1 { if let Some(arg) = call.args.first() {