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

feat(router): auto-unwrap default exports when lazy loading #47586

Closed
wants to merge 2 commits into from

Conversation

alxhub
Copy link
Member

@alxhub alxhub commented Sep 29, 2022

When using loadChildren or loadComponent, a common pattern is to pass a function that returns a Promise from a dynamic import:

{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyCmp),
}

The .then part of the expression selects the particular exported component symbol from the dynamically imported ES module.

ES modules can have a "default export", created with the export default modifier:

@Component({...})
export default class LazyCmp { ... }

This default export is made available to dynamic imports under the well- known key of 'default', per the ES module spec:
https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records

This commit adds a feature to the router to automatically dereference such default exports. With this logic, when export default is used, a .then operation to select the particular exported symbol is no longer required:

{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}

The above loadComponent operation will automatically use the default export of the lazy-file ES module.

This functionality works for loadChildren as well.

@alxhub alxhub requested a review from atscott September 29, 2022 21:25
@alxhub alxhub added area: router target: major This PR is targeted for the next major release labels Sep 29, 2022
@ngbot ngbot bot added this to the Backlog milestone Sep 29, 2022
@alxhub alxhub force-pushed the v15/router/default branch 3 times, most recently from 6444f44 to 2b5209b Compare September 29, 2022 23:51
@angular-robot angular-robot bot added the feature Issue that requests a new feature label Sep 29, 2022
Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

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

reviewed-for: public-api

@pullapprove pullapprove bot requested a review from dylhunn September 30, 2022 15:11
Copy link
Contributor

@AndrewKushnir AndrewKushnir left a comment

Choose a reason for hiding this comment

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

Reviewed-for: public-api

(just one comment about the DX for error cases)

expect(root.nativeElement.innerHTML).toContain('default exported');
});

it('should work for loadChildren', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering what the DX would look like in case of missing default export? Can we add a test to check that?

I was thinking of a couple scenarios:

  • no exports at all (we can have an error message explaining this)
  • no default exports, but other symbols are exported (we can also throw a message explaining that)

WDYT?

(we can also do that in a followup PR if needed, it's not a blocker)

Copy link
Contributor

Choose a reason for hiding this comment

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

TypeScript will fail in both cases:

If there are no exports, the TypeScript compiler will error with File 'the/imported/thing' is not a module.

If there is no default export, it would fail the DefaultExport<T> type.

Copy link
Member

@pkozlowski-opensource pkozlowski-opensource left a comment

Choose a reason for hiding this comment

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

LGTM

Reviewed-for: public-api
Reviewed-for: fw-core

@pkozlowski-opensource pkozlowski-opensource added the action: merge The PR is ready for merge by the caretaker label Oct 1, 2022
@AndrewKushnir
Copy link
Contributor

FYI, it looks like this change would require an update to the local patches in g3, adding the "blocked" label for now...

@AndrewKushnir AndrewKushnir added state: blocked action: merge The PR is ready for merge by the caretaker and removed action: merge The PR is ready for merge by the caretaker labels Oct 3, 2022
@alxhub alxhub added merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note and removed state: blocked labels Oct 4, 2022
@alxhub
Copy link
Member Author

alxhub commented Oct 4, 2022

Caretaker: a g3 patch (cl/478602165) is required

@@ -357,7 +357,7 @@ export interface IsActiveMatchOptions {
}

// @public
export type LoadChildren = LoadChildrenCallback;
export type LoadChildren = LoadChildrenCallback | ɵDeprecatedLoadChildren;
Copy link
Contributor

Choose a reason for hiding this comment

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

ɵDeprecatedLoadChildren now appears in the public API even though it's not exported and is unused externally. What about moving the LoadChildren type to its own file that we patch in g3 to add ɵDeprecatedLoadChildren?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep - that's fine. The goal is to not need to touch the definition of LoadChildren at all in the patch file - that's what leads to brittle patch files.

packages/router/src/deprecated_load_children.ts Outdated Show resolved Hide resolved
@@ -122,6 +123,9 @@ export class RouterConfigLoader {

private loadModuleFactoryOrRoutes(loadChildren: LoadChildren):
Observable<NgModuleFactory<any>|Routes> {
if (typeof loadChildren === 'string') {
return deprecatedLoadChildrenString(this.injector, loadChildren);
Copy link
Contributor

Choose a reason for hiding this comment

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

What if we unconditionally call deprecatedLoadChildrenString from here and make it an empty function externally? We at least have a single file we can patch in g3 then.

Copy link
Member Author

Choose a reason for hiding this comment

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

This doesn't work, because the typeof needs to be performed within loadModuleFactoryOrRoutes to narrow the type of loadChildren for the rest of the function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, this can work with a cast, so I applied that change.

@AndrewKushnir AndrewKushnir added action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews and removed action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note labels Oct 4, 2022
This commit breaks out the code in @angular/router needed to support the
deprecated `loadChildren` signature into a separate file, which reduces the
opportunity for conflicts when patching that behavior in g3.
When using `loadChildren` or `loadComponent`, a common pattern is to pass
a function that returns a `Promise` from a dynamic import:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyCmp),
}
```

The `.then` part of the expression selects the particular exported
component symbol from the dynamically imported ES module.

ES modules can have a "default export", created with the `export default`
modifier:

```typescript
@component({...})
export default class LazyCmp { ... }
```

This default export is made available to dynamic imports under the well-
known key of `'default'`, per the ES module spec:
https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records

This commit adds a feature to the router to automatically dereference such
default exports. With this logic, when `export default` is used, a `.then`
operation to select the particular exported symbol is no longer required:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}
```

The above `loadComponent` operation will automatically use the `default`
export of the `lazy-file` ES module.

This functionality works for `loadChildren` as well.
@alxhub alxhub added action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note and removed action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews labels Oct 4, 2022
@ngbot
Copy link

ngbot bot commented Oct 4, 2022

I see that you just added the action: merge label, but the following checks are still failing:
    failure status "ci/angular: size" is failing
    failure status "ci/circleci: test" is failing
    failure status "google3" is failing

If you want your PR to be merged, it has to pass all the CI checks.

If you can't get the PR to a green state due to flakes or broken main, please try rebasing to main and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help.

@alxhub alxhub added action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note and removed action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note labels Oct 4, 2022
@AndrewKushnir
Copy link
Contributor

This PR was merged into the repository by commit da58801.

AndrewKushnir pushed a commit that referenced this pull request Oct 4, 2022
When using `loadChildren` or `loadComponent`, a common pattern is to pass
a function that returns a `Promise` from a dynamic import:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyCmp),
}
```

The `.then` part of the expression selects the particular exported
component symbol from the dynamically imported ES module.

ES modules can have a "default export", created with the `export default`
modifier:

```typescript
@component({...})
export default class LazyCmp { ... }
```

This default export is made available to dynamic imports under the well-
known key of `'default'`, per the ES module spec:
https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records

This commit adds a feature to the router to automatically dereference such
default exports. With this logic, when `export default` is used, a `.then`
operation to select the particular exported symbol is no longer required:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}
```

The above `loadComponent` operation will automatically use the `default`
export of the `lazy-file` ES module.

This functionality works for `loadChildren` as well.

PR Close #47586
atscott pushed a commit to atscott/angular that referenced this pull request Oct 4, 2022
…47586)

When using `loadChildren` or `loadComponent`, a common pattern is to pass
a function that returns a `Promise` from a dynamic import:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyCmp),
}
```

The `.then` part of the expression selects the particular exported
component symbol from the dynamically imported ES module.

ES modules can have a "default export", created with the `export default`
modifier:

```typescript
@component({...})
export default class LazyCmp { ... }
```

This default export is made available to dynamic imports under the well-
known key of `'default'`, per the ES module spec:
https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records

This commit adds a feature to the router to automatically dereference such
default exports. With this logic, when `export default` is used, a `.then`
operation to select the particular exported symbol is no longer required:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}
```

The above `loadComponent` operation will automatically use the `default`
export of the `lazy-file` ES module.

This functionality works for `loadChildren` as well.

PR Close angular#47586
atscott pushed a commit to atscott/angular that referenced this pull request Oct 4, 2022
…47586)

When using `loadChildren` or `loadComponent`, a common pattern is to pass
a function that returns a `Promise` from a dynamic import:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyCmp),
}
```

The `.then` part of the expression selects the particular exported
component symbol from the dynamically imported ES module.

ES modules can have a "default export", created with the `export default`
modifier:

```typescript
@component({...})
export default class LazyCmp { ... }
```

This default export is made available to dynamic imports under the well-
known key of `'default'`, per the ES module spec:
https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records

This commit adds a feature to the router to automatically dereference such
default exports. With this logic, when `export default` is used, a `.then`
operation to select the particular exported symbol is no longer required:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}
```

The above `loadComponent` operation will automatically use the `default`
export of the `lazy-file` ES module.

This functionality works for `loadChildren` as well.

PR Close angular#47586
mprobst pushed a commit to mprobst/angular that referenced this pull request Oct 5, 2022
This commit breaks out the code in @angular/router needed to support the
deprecated `loadChildren` signature into a separate file, which reduces the
opportunity for conflicts when patching that behavior in g3.

PR Close angular#47586
mprobst pushed a commit to mprobst/angular that referenced this pull request Oct 5, 2022
…47586)

When using `loadChildren` or `loadComponent`, a common pattern is to pass
a function that returns a `Promise` from a dynamic import:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyCmp),
}
```

The `.then` part of the expression selects the particular exported
component symbol from the dynamically imported ES module.

ES modules can have a "default export", created with the `export default`
modifier:

```typescript
@component({...})
export default class LazyCmp { ... }
```

This default export is made available to dynamic imports under the well-
known key of `'default'`, per the ES module spec:
https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records

This commit adds a feature to the router to automatically dereference such
default exports. With this logic, when `export default` is used, a `.then`
operation to select the particular exported symbol is no longer required:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}
```

The above `loadComponent` operation will automatically use the `default`
export of the `lazy-file` ES module.

This functionality works for `loadChildren` as well.

PR Close angular#47586
yharaskrik pushed a commit to yharaskrik/angular that referenced this pull request Oct 5, 2022
This commit breaks out the code in @angular/router needed to support the
deprecated `loadChildren` signature into a separate file, which reduces the
opportunity for conflicts when patching that behavior in g3.

PR Close angular#47586
yharaskrik pushed a commit to yharaskrik/angular that referenced this pull request Oct 5, 2022
…47586)

When using `loadChildren` or `loadComponent`, a common pattern is to pass
a function that returns a `Promise` from a dynamic import:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyCmp),
}
```

The `.then` part of the expression selects the particular exported
component symbol from the dynamically imported ES module.

ES modules can have a "default export", created with the `export default`
modifier:

```typescript
@component({...})
export default class LazyCmp { ... }
```

This default export is made available to dynamic imports under the well-
known key of `'default'`, per the ES module spec:
https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records

This commit adds a feature to the router to automatically dereference such
default exports. With this logic, when `export default` is used, a `.then`
operation to select the particular exported symbol is no longer required:

```typescript
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}
```

The above `loadComponent` operation will automatically use the `default`
export of the `lazy-file` ES module.

This functionality works for `loadChildren` as well.

PR Close angular#47586
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Nov 4, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: router feature Issue that requests a new feature merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants