Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add no-op tier import API #9865

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Call this Deferred

}

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 = unsafe_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 = unsafe_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 --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"
},
"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;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interface DeferredImport<T> {
   onReady(callback: (T) => void): void;
}
  • No need for a cleanup. No bundler cleans up their already loaded JS modules (AFAIK), and if they do they'll set a precedent of how runtime code should drop references.
  • The content is the default value, not the EM module's whole scope. It's so, so, so much easier that way. Means we avoid partial-value caching (remember that caching a promise / callback-able thing means we've caching not only the end value but also the promise / work to get it, else multiple of them will be constructed)
  • No need to expose the module value directly. You need to onReady() to get access to it


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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend that these only provide access to the default exported value, not the module itself.

It's the bundlers responsibility to ensure that devs can't shoot themselves in the foot by doing things like importing the one file both deferred and non-deferred because they're putting multiple values in the one file.

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>
52 changes: 52 additions & 0 deletions packages/examples/phases/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, {StrictMode, Suspense, useState} from 'react';
import ReactDOM from 'react-dom';

import Tier1 from './tier1';
const DeferredTier2 =
unsafe_importDeferredForDisplay<typeof import('./tier2')>('./tier2');
const DeferredTier3 =
unsafe_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