Skip to content

Commit

Permalink
Merge 4ad1af2 into 6a8d5e9
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeLane authored Aug 6, 2024
2 parents 6a8d5e9 + 4ad1af2 commit e2a20f1
Show file tree
Hide file tree
Showing 31 changed files with 606 additions and 115 deletions.
2 changes: 2 additions & 0 deletions crates/parcel_core/src/types/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub(crate) fn convert_priority(
DependencyKind::Export => Priority::Sync,
DependencyKind::Require => Priority::Sync,
DependencyKind::File => Priority::Sync,
DependencyKind::DeferredForDisplayImport => Priority::Tier,
DependencyKind::DeferredImport => Priority::Tier,
}
}

Expand All @@ -33,13 +35,17 @@ pub(crate) fn convert_specifier_type(
DependencyKind::Worklet => SpecifierType::Url,
DependencyKind::Url => SpecifierType::Url,
DependencyKind::File => SpecifierType::Custom,
DependencyKind::DeferredForDisplayImport => SpecifierType::Esm,
DependencyKind::DeferredImport => SpecifierType::Esm,
}
}

#[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::*;

Expand All @@ -55,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(
Expand Down Expand Up @@ -167,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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion packages/bundlers/default/src/DefaultBundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const dependencyPriorityEdges = {
sync: 1,
parallel: 2,
lazy: 3,
tier: 4,
};

type DependencyBundleGraph = ContentGraph<
Expand Down Expand Up @@ -845,6 +846,7 @@ function createIdealGraph(

if (
dependency.priority !== 'sync' &&
dependency.priority !== 'tier' &&
dependencyBundleGraph.hasContentKey(dependency.id)
) {
let assets = assetGraph.getDependencyAssets(dependency);
Expand Down Expand Up @@ -872,7 +874,10 @@ function createIdealGraph(
}
}

if (dependency.priority !== 'sync') {
if (
dependency.priority !== 'sync' &&
dependency.priority !== 'tier'
) {
actions.skipChildren();
}
return;
Expand Down
1 change: 1 addition & 0 deletions packages/core/core/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const Priority = {
sync: 0,
parallel: 1,
lazy: 2,
tier: 3,
};

// Must match package_json.rs in node-resolver-rs.
Expand Down
1 change: 1 addition & 0 deletions packages/core/core/test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const DEFAULT_OPTIONS: ParcelOptions = {
...DEFAULT_FEATURE_FLAGS,
exampleFeature: false,
parcelV3: false,
tieredImports: false,
importRetry: false,
},
};
Expand Down
1 change: 1 addition & 0 deletions packages/core/feature-flags/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
parcelV3: false,
importRetry: false,
ownedResolverStructures: false,
tieredImports: false,
};

let featureFlagValues: FeatureFlags = {...DEFAULT_FEATURE_FLAGS};
Expand Down
7 changes: 7 additions & 0 deletions packages/core/feature-flags/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ export type FeatureFlags = {|
* Enable resolver refactor into owned data structures.
*/
ownedResolverStructures: boolean,
/**
* Tiered imports API
* Enable tier imports
*
* Tier imports allow developers to have control over when code is loaded
*/
+tieredImports: boolean,
|};
4 changes: 2 additions & 2 deletions packages/core/integration-tests/test/sourcemaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand All @@ -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',
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/core/types-internal/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/examples/phases/.parcelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "@parcel/config-default"
}
20 changes: 20 additions & 0 deletions packages/examples/phases/package.json
Original file line number Diff line number Diff line change
@@ -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 -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"
},
"devDependencies": {
"parcel": "2.12.0"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
14 changes: 14 additions & 0 deletions packages/examples/phases/phases.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type ModuleRef<_> = string;
type ErrorMessage = 'You must annotate type with "<typeof import(\'xyz\')>"';

interface DeferredImport<T extends {default: any}> {
onReady(resource: (mod: T['default']) => void): void;
}

declare function importDeferredForDisplay<T extends any | void = void>(
source: T extends void ? ErrorMessage : ModuleRef<T>,
): DeferredImport<T>;

declare function importDeferred<T extends any | void = void>(
source: T extends void ? ErrorMessage : ModuleRef<T>,
): DeferredImport<T>;
14 changes: 14 additions & 0 deletions packages/examples/phases/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Parcel | React Phases</title>
</head>
<body>
<div id="app"></div>

<script src="./index.tsx" type="module"></script>
</body>
</html>
51 changes: 51 additions & 0 deletions packages/examples/phases/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, {StrictMode, Suspense, useState} from 'react';
import ReactDOM from 'react-dom';

import Tier1 from './tier1';
const DeferredTier2 =
importDeferredForDisplay<typeof import('./tier2')>('./tier2');
const DeferredTier3 = importDeferred<typeof import('./tier3')>('./tier3');

import {deferredLoadComponent} from './utils';

const Tier2 = deferredLoadComponent(DeferredTier2);
const Tier3Instance1 = deferredLoadComponent(DeferredTier3);
const Tier3Instance2 = deferredLoadComponent(DeferredTier3);

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

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

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

export default function Lazy() {
return <div>Lazy</div>;
}
5 changes: 5 additions & 0 deletions packages/examples/phases/src/tier1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export default function Tier1() {
return <div>Tier 1</div>;
}
6 changes: 6 additions & 0 deletions packages/examples/phases/src/tier2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';

export default function Tier2({enabled}: {enabled: boolean}) {
if (enabled) return <div>Tier 2</div>;
throw new Error('Enabled prop missing');
}
5 changes: 5 additions & 0 deletions packages/examples/phases/src/tier3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export default function Tier3() {
return <div>Tier 3</div>;
}
57 changes: 57 additions & 0 deletions packages/examples/phases/src/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, {
ComponentType,
ForwardRefExoticComponent,
ForwardedRef,
MemoExoticComponent,
PropsWithChildren,
PropsWithoutRef,
RefAttributes,
forwardRef,
memo,
} from 'react';

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

if (globalThis.deferredComponentMap.has(resource)) {
return globalThis.deferredComponentMap.get(resource);
}

let Component: ComponentType | undefined;
let loader = new Promise(resolve => {
resource.onReady(loaded => {
Component = loaded;
resolve(loaded);
});
});

const wrapper = function DeferredComponent(
props: PropsWithChildren<P>,
ref: ForwardedRef<ComponentType<P>>,
) {
if (Component) {
return <Component {...props} ref={ref} />;
}

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;
}
8 changes: 8 additions & 0 deletions packages/examples/phases/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"typeRoots": ["phases.d.ts"],
"jsx": "react",
"lib": ["ESNext", "DOM"],
"esModuleInterop": true
}
}
1 change: 1 addition & 0 deletions packages/packagers/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit e2a20f1

Please sign in to comment.