Skip to content

Commit

Permalink
feat: enable live code inclusion for consuming applications (#2642)
Browse files Browse the repository at this point in the history
Packages are marked as side effect free or have the files that include side effects listed in package.json. This allows bundlers to discard code not in the direct dependency tree of an import.
Adds functions for registering components
Moved to generating a single file per react wrpper component and a barrel file
Update tests to use registerComponent calls
fixing render problem when given and surname are null on personType objects
move mgt-components to module type es2020 for dynamic imports
refactored to only lazy load the person-card code when opening the flyout
update list of components registered by registerMgtComponents
move sample app to use lazy + Suspense to chunk up component usage
adding documentation for tree shaking support
  • Loading branch information
gavinbarron committed Oct 24, 2023
1 parent d27ffa7 commit c41d719
Show file tree
Hide file tree
Showing 115 changed files with 1,409 additions and 696 deletions.
37 changes: 37 additions & 0 deletions cem-plugins/mgt-tag-plugin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { resolveModuleOrPackageSpecifier } from '@custom-elements-manifest/analyzer/src/utils/index.js';
export default function mgtTagPlugin() {
function isCustomRegistration(node) {
// this would be better if we tested arg[0] for a string literal
return node?.expression?.getText() === 'registerComponent' && node.arguments.length >= 2;
}
// Write a custom plugin
return {
// Make sure to always give your plugins a name, this helps when debugging
name: 'mgt-tag-plugin',
// Runs for all modules in a project, before continuing to the analyzePhase
collectPhase({ ts, node, context }) {},
// Runs for each module
analyzePhase({ ts: TS, node, moduleDoc, context }) {
if (isCustomRegistration(node)) {
//do things...
if (context.dev) console.log(node);

var elementClass = node.arguments[1].text;
var elementTag = node.arguments[0].text;
const definitionDoc = {
kind: 'custom-element-definition',
name: elementTag,
declaration: {
name: elementClass,
...resolveModuleOrPackageSpecifier(moduleDoc, context, elementClass)
}
};
moduleDoc.exports = [...(moduleDoc.exports || []), definitionDoc];
}
},
// Runs for each module, after analyzing, all information about your module should now be available
moduleLinkPhase({ moduleDoc, context }) {},
// Runs after modules have been parsed and after post-processing
packageLinkPhase({ customElementsManifest, context }) {}
};
}
5 changes: 5 additions & 0 deletions custom-elements-manifest.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import mgtTagPlugin from './cem-plugins/mgt-tag-plugin.mjs';

export default {
plugins: [mgtTagPlugin()]
};
8 changes: 6 additions & 2 deletions lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"packages": ["packages/*", "packages/providers/*", "samples/react-contoso"],
"packages": [
"packages/*",
"packages/providers/*",
"samples/*"
],
"npmClient": "yarn",
"version": "independent"
}
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
},
"scripts": {
"init": "yarn && yarn build",
"analyze": "custom-elements-manifest analyze --litelement --globs \"./packages/*/src/**/*.ts\"",
"build": "npm run prettier:check && npm run clean && lerna run --scope '@microsoft/*' build",
"analyze": "custom-elements-manifest analyze --globs \"./packages/*/src/**/*.ts\"",
"build": "npm run prettier:check && npm run clean && lerna run build --scope '@microsoft/*'",
"build:dev": "npm run prettier:check && lerna run build --scope '@microsoft/*' --ignore '@microsoft/mgt' --ignore '@microsoft/mgt-spf*' --ignore '@microsoft/mgt-sharepoint-provider' --ignore '@microsoft/mgt-electron-provider' --ignore '@microsoft/mgt-teamsfx-provider' --ignore '@microsoft/mgt-proxy-provider'",
"build:compile": "npm run prettier:check && npm run clean && lerna run --scope @microsoft/* build:compile",
"build:compile": "npm run prettier:check && npm run clean && lerna run build:compile --scope '@microsoft/*'",
"build:mgt": "cd ./packages/mgt && npm run build",
"build:mgt-element": "cd ./packages/mgt-element && npm run build",
"build:mgt-components": "cd ./packages/mgt-components && npm run build",
Expand Down
98 changes: 91 additions & 7 deletions packages/mgt-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The components can be used on their own, but they are at their best when they ar
```html
<script type="module">
import {Providers} from '@microsoft/mgt-element';
import {Msal2Provider} from '@microsoft/mgt-msal2-provider';
import {Msal2Provider} from '@microsoft/mgt-msal2-provider/dist/es6/exports';
// import the components
import '@microsoft/mgt-components';
Expand All @@ -62,6 +62,48 @@ The components can be used on their own, but they are at their best when they ar
<mgt-agenda group-by-day></mgt-agenda>
```

## Tree shaking/Live code inclusion

By default importing anything from the root of the `@microsoft/mgt-components` package triggers a side effect causing the registration of all components as custom elements with the browser. If you are using a bundler that can perform "tree shaking" then there are two steps to ensure that your bundler can correctly determine which pieces of code to include in you bundle.

- Use the `@microsoft/mgt-components/dist/es6/exports` path to import your dependencies. This code path has no side effects unlike the root path.
- Explicitly register each component that will be used in your application using the appropriate function. For each component there is a registration function `registerMgt{Name}Component()`, e.g. `registerMgtLoginComponent()`.

In cases where a component has a dependency on other components these are all registered in the registration function. For example, the mgt-login component uses an mgt-person internally, so `registerMgtLoginComponent()` calls `registerMgtPersonComponent()`.`

### Why have the auto registration side effect at the root?

Versions 3.1.x and lower all provide automatic registration of the web components as a side effect. By keeping this behavior we prevent this change from being a breaking change. We may refactor this behavior with version 4 to move the side effect based component registration to an explicit function call.

### Why use a explicit function call for component registration?

This removes the registration of a component from being a side effect of importing the component, this allows for imperative disambiguation of web components without the need to rely on dynamic imports to ensure that disambiguation is configured before the import of the `@microsoft/mgt-components` library happens. It also provides for greater developer control.

### Example usage

```html
<script type="module">
import {Providers} from '@microsoft/mgt-element';
import {Msal2Provider} from '@microsoft/mgt-msal2-provider/dist/es6/exports';
// import the registration functions
import {
registerMgtLoginComponent,
registerMgtAgendaComponent
} from '@microsoft/mgt-components/dist/es6/exports';
// register the components
registerMgtLoginComponent();
registerMgtAgendaComponent();
// initialize the auth provider globally
Providers.globalProvider = new Msal2Provider({clientId: 'clientId'});
</script>

<mgt-login></mgt-login>
<mgt-person person-query="Bill Gates" person-card="hover"></mgt-person>
<mgt-agenda group-by-day></mgt-agenda>
```

## <a id="disambiguation">Disambiguation</a>

Expand All @@ -71,17 +113,27 @@ To mitigate this challenge we built the [`mgt-spfx`](https://github.com/microsof

To allow developers to build web parts using the latest version of MGT and load them on pages along with web parts that use v2.x of MGT, we've added a new disambiguation feature to MGT. Using this feature developers can specify a unique string to add to the tag name of all MGT web components in their application.

### Usage in standard HTML and JavaScript

### Usage in standard HTML and JavaScript with explicit component registration

The earlier example can be updated to use the disambiguation feature as follows:

```html
<script type="module">
import { Providers, customElementHelper } from '@microsoft/mgt-element';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider/dist/es6/exports';
import {
registerMgtLoginComponent,
registerMgtAgendaComponent
} from '@microsoft/mgt-components/dist/es6/exports';
// configure disambiguation
customElementHelper.withDisambiguation('contoso');
// register the components
registerMgtLoginComponent();
registerMgtAgendaComponent();
// initialize the auth provider globally
Providers.globalProvider = new Msal2Provider({clientId: 'clientId'});
Expand All @@ -94,7 +146,32 @@ The earlier example can be updated to use the disambiguation feature as follows:
<mgt-contoso-agenda group-by-day></mgt-contoso-agenda>
```

> Note: the `import` of `mgt-components` must use a dynamic import to ensure that the disambiguation is applied before the components are imported.
> Note: `withDisambiguation('foo')` must be called before registering the the desired components.
### Usage in standard HTML and JavaScript with dynamic imports

The earlier example can be updated to use the disambiguation feature as follows:

```html
<script type="module">
import { Providers, customElementHelper } from '@microsoft/mgt-element';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider/dist/es6/exports';
// configure disambiguation
customElementHelper.withDisambiguation('contoso');
// initialize the auth provider globally
Providers.globalProvider = new Msal2Provider({clientId: 'clientId'});
// import the components using dynamic import to avoid hoisting
import('@microsoft/mgt-components');
</script>

<mgt-contoso-login></mgt-contoso-login>
<mgt-contoso-person person-query="Bill Gates" person-card="hover"></mgt-contoso-person>
<mgt-contoso-agenda group-by-day></mgt-contoso-agenda>
```

> Note: the `import` of `mgt-components` must use a dynamic import to ensure that the disambiguation is applied before the components are imported and the automatic registration is performed.
When developing SharePoint Framework web parts the pattern for using disambiguation is based on whether or not the MGT React wrapper library is being used. If you are using React then the helper utility in the [`mgt-spfx-utils`](https://github.com/microsoftgraph/microsoft-graph-toolkit/tree/main/packages/mgt-spfx-utils) package should be used. SharePoint Framework web part example usages are provided below.

Expand All @@ -109,15 +186,20 @@ import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { Providers } from '@microsoft/mgt-element';
import { SharePointProvider } from '@microsoft/mgt-sharepoint-provider';
import { customElementHelper } from '@microsoft/mgt-element/dist/es6/components/customElementHelper';
import { registerMgtLoginComponent } from '@microsoft/mgt-components';

export default class MgtWebPart extends BaseClientSideWebPart<Record<string, unknown>> {

protected onInit(): Promise<void> {
customElementHelper.withDisambiguation('foo');

// register the component
registerMgtLoginComponent();

if (!Providers.globalProvider) {
Providers.globalProvider = new SharePointProvider(this.context);
}
return import('@microsoft/mgt-components').then(() => super.onInit());
return super.onInit();
}

public render(): void {
Expand All @@ -131,6 +213,8 @@ export default class MgtWebPart extends BaseClientSideWebPart<Record<string, unk

### Usage in a SharePoint web part using React

When using Microsoft Graph Toolkit components via the `@microsoft/mgt-react` wrapper the underlying custom element registration is handled when generating the wrapping React function component, meaning that the register calls are made when each component is imported. As such the pattern for using disambiguation is unchanged with the introduction of tree shaking compatibility.

The `lazyLoadComponent` helper function from [`mgt-spfx-utils`](https://github.com/microsoftgraph/microsoft-graph-toolkit/tree/main/packages/mgt-spfx-utils) leverages `React.lazy` and `React.Suspense` to asynchronously load the components which have a direct dependency on `@microsoft/mgt-react` from the top level web part component.

A complete example is available in the [React SharePoint Web Part Sample](https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/main/samples/sp-webpart/src/webparts/mgtDemo/MgtDemoWebPart.ts).
Expand Down Expand Up @@ -182,7 +266,7 @@ When using an `import` statement the import statement is hoisted and executed be
```typescript
// static import via a statement
import { Providers, customElementHelper } from '@microsoft/mgt-element';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider/dist/es6/exports';

customElementHelper.withDisambiguation('contoso');
Providers.globalProvider = new Msal2Provider({clientId: 'clientId'});
Expand All @@ -201,7 +285,7 @@ import('@microsoft/mgt-components').then(() => {
```typescript
// static import via a statement
import { Provider } from '@microsoft/mgt-element';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider';
import { Msal2Provider } from '@microsoft/mgt-msal2-provider/dist/es6/exports';
import '@microsoft/mgt-components';

Providers.globalProvider = new Msal2Provider({clientId: 'clientId'});
Expand Down
4 changes: 4 additions & 0 deletions packages/mgt-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"sass": "gulp sass --cwd .",
"sass:watch": "gulp watchSass --cwd ."
},
"sideEffects": [
"./dist/es6/index.js",
"./src/index.ts"
],
"dependencies": {
"@fluentui/web-components": "^2.5.12",
"@microsoft/mgt-element": "*",
Expand Down
4 changes: 4 additions & 0 deletions packages/mgt-components/src/components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import './mgt-messages/mgt-messages';
import './mgt-organization/mgt-organization';
import './mgt-profile/mgt-profile';
import './mgt-theme-toggle/mgt-theme-toggle';
import './sub-components/mgt-spinner/mgt-spinner';

export * from './mgt-agenda/mgt-agenda';
export * from './mgt-file/mgt-file';
Expand All @@ -47,3 +48,6 @@ export * from './mgt-messages/mgt-messages';
export * from './mgt-organization/mgt-organization';
export * from './mgt-profile/mgt-profile';
export * from './mgt-theme-toggle/mgt-theme-toggle';
export * from './sub-components/mgt-spinner/mgt-spinner';
// include preview components here for ease of import into mgt-react
export * from './preview';
16 changes: 12 additions & 4 deletions packages/mgt-components/src/components/mgt-agenda/mgt-agenda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
import { html, TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { Providers, ProviderState, MgtTemplatedComponent, mgtHtml, customElement } from '@microsoft/mgt-element';
import { Providers, ProviderState, MgtTemplatedComponent, mgtHtml } from '@microsoft/mgt-element';
import '../../styles/style-helper';
import '../mgt-person/mgt-person';
import { styles } from './mgt-agenda-css';
import { getEventsPageIterator, getEventsQueryPageIterator } from './mgt-agenda.graph';
import { SvgIcon, getSvg } from '../../utils/SvgHelper';
import { MgtPeople } from '../mgt-people/mgt-people';
import { MgtPeople, registerMgtPeopleComponent } from '../mgt-people/mgt-people';
import { registerFluentComponents } from '../../utils/FluentComponents';
import { fluentCard } from '@fluentui/web-components';
import { classMap } from 'lit/directives/class-map.js';
registerFluentComponents(fluentCard);
import { registerComponent } from '@microsoft/mgt-element';

/**
* Web Component which represents events in a user or group calendar.
Expand All @@ -45,7 +45,15 @@ registerFluentComponents(fluentCard);
* @cssprop --event-location-color - {Color} Event location color
* @cssprop --event-attendees-color - {Color} Event attendees color
*/
@customElement('agenda')

export const registerMgtAgendaComponent = () => {
registerFluentComponents(fluentCard);
// register dependent components
registerMgtPeopleComponent();
// register self
registerComponent('agenda', MgtAgenda);
};

export class MgtAgenda extends MgtTemplatedComponent {
/**
* Array of styles to apply to the element. The styles should be defined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@

import { User } from '@microsoft/microsoft-graph-types';
import { html, TemplateResult } from 'lit';
import { TeamsHelper, customElement } from '@microsoft/mgt-element';
import { TeamsHelper } from '@microsoft/mgt-element';
import { classMap } from 'lit/directives/class-map.js';

import { getEmailFromGraphEntity } from '../../graph/graph.people';
import { BasePersonCardSection } from '../BasePersonCardSection';
import { styles } from './mgt-contact-css';
import { getSvg, SvgIcon } from '../../utils/SvgHelper';
import { strings } from './strings';
import { registerComponent } from '@microsoft/mgt-element';

/**
* Represents a contact part and its metadata
Expand All @@ -31,15 +32,17 @@ interface IContactPart {

type Protocol = 'mailto:' | 'tel:';

export const registerMgtContactComponent = () => {
registerComponent('contact', MgtContact);
};

/**
* The contact details subsection of the person card
*
* @export
* @class MgtContact
* @extends {MgtTemplatedComponent}
*/
@customElement('contact')
// @customElement('mgt-contact')
export class MgtContact extends BasePersonCardSection {
/**
* Array of styles to apply to the element. The styles should be defined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
GraphPageIterator,
Providers,
ProviderState,
customElement,
mgtHtml,
MgtTemplatedComponent
} from '@microsoft/mgt-element';
Expand Down Expand Up @@ -42,14 +41,21 @@ import { getSvg, SvgIcon } from '../../utils/SvgHelper';
import { OfficeGraphInsightString, ViewType } from '../../graph/types';
import { styles } from './mgt-file-list-css';
import { strings } from './strings';
import { MgtFile } from '../mgt-file/mgt-file';
import { MgtFileUploadConfig } from './mgt-file-upload/mgt-file-upload';
import { MgtFile, registerMgtFileComponent } from '../mgt-file/mgt-file';
import { MgtFileUploadConfig, registerMgtFileUploadComponent } from './mgt-file-upload/mgt-file-upload';

import { fluentProgressRing } from '@fluentui/web-components';
import { registerFluentComponents } from '../../utils/FluentComponents';
import { CardSection } from '../BasePersonCardSection';
import { registerComponent } from '@microsoft/mgt-element';

registerFluentComponents(fluentProgressRing);
export const registerMgtFileListComponent = () => {
registerFluentComponents(fluentProgressRing);

registerMgtFileComponent();
registerMgtFileUploadComponent();
registerComponent('file-list', MgtFileList);
};

/**
* The File List component displays a list of multiple folders and files by
Expand Down Expand Up @@ -77,8 +83,6 @@ registerFluentComponents(fluentProgressRing);
* @cssprop --show-more-button-border-bottom-left-radius - {String} the "show more" button bottom left border radius. Default value is 8px;
* @cssprop --progress-ring-size -{String} Progress ring height and width. Default value is 24px.
*/

@customElement('file-list')
export class MgtFileList extends MgtTemplatedComponent implements CardSection {
@state() private _isCompact = false;
/**
Expand Down

0 comments on commit c41d719

Please sign in to comment.