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 custom elements manifest #31673

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual regressions to review in the fluentuiv9 Visual Regression Report

Avatar Converged 2 screenshots
Image Name Diff(in Pixels) Image Type
Avatar Converged.badgeMask.normal.chromium.png 2 Changed
Avatar Converged.badgeMask - RTL.normal.chromium.png 1 Changed

"type": "prerelease",
"comment": "Add a custom elements manifest",
"packageName": "@fluentui/web-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
10 changes: 10 additions & 0 deletions packages/web-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ import { ButtonDefinition, FluentDesignSystem } from '@fluentui/web-components';
ButtonDefinition.define(FluentDesignSystem.registry);
```

## Developer Experience

For convenience we have included a [CEM (custom elements manifest)](https://github.com/webcomponents/custom-elements-manifest).

```js
import CEM from '@fluentui/custom-elements.json';
```

We have also included an [HTML custom data file for VS Code](./vs-code.md).

## Development

To start the component development environment, run `yarn start`.
Expand Down
35 changes: 35 additions & 0 deletions packages/web-components/custom-elements-manifest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { tagNameFix, typescriptTypeTextSanitize } from "./custom-elements-manifest.plugins.js";
import { customElementVsCodePlugin } from "custom-element-vs-code-integration";

export default {
/** Globs to analyze */
globs: ["src/**/*.ts"],
/** Globs to exclude */
exclude: [
"*.js",
"*.ts",
"src/helpers.stories.ts",
"src/helpers.tests.ts",
"src/index-rollup.ts",
"src/utils/benchmark-wrapper.ts",
"src/**/*.bench.ts",
"src/**/*.spec.ts",
"src/**/*.stories.ts",
"src/**/define.ts",
"src/**/index.ts",
"src/**/*.md"
],
/** Directory to output CEM to */
outdir: "dist",
/** Run in dev mode, provides extra logging */
dev: false,
/** Enable special handling for fast */
fast: true,
plugins: [
tagNameFix(),
typescriptTypeTextSanitize(),
customElementVsCodePlugin({
outdir: "dist"
})
],
};
167 changes: 167 additions & 0 deletions packages/web-components/custom-elements-manifest.plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { readFileSync } from "fs";
import path from "path";
import ts from "typescript";
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Copy link
Contributor

Choose a reason for hiding this comment

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

You can get dirname from import.meta.dirname.


const getTagNameFromCommentInDefinitionFile = function (definitionPathName) {
const indexFilePath = path.resolve(__dirname, `./${definitionPathName}`);
let name;

try {
let sourceFile = ts.createSourceFile(
indexFilePath,
readFileSync(indexFilePath).toString(),
ts.ScriptTarget.ES2015,
/*setParentNodes */ true
);

if (Array.isArray(sourceFile.statements)) {
sourceFile.statements.forEach((statement) => {
if (Array.isArray(statement.jsDoc) && statement.jsDoc[0].tags !== undefined) {
statement.jsDoc.forEach((jsDoc) => {
if (Array.isArray(jsDoc.tags)) {
jsDoc.tags.forEach((tag) => {
if (typeof tag.comment === "string" && tag.comment.startsWith("HTML Element:")) {
name = tag.comment.match(/<(.*)>/)[1].replace("\\", "");
}
});
}
})
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: to improve readability, maybe test negative cases and return? Something like:

if (!Array.isArray(...)) {
  return;
}
sourceFile.statements.forEach(...);

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree with @marchbox's suggestion these nested ifs could be easier to read. Can sourceFile.statements, statement.jsDoc, or jsDoc.tags not be arrays?

} catch (err) {
// do nothing
}

return name;
}

const resolveDefinitionFilePath = function(filePathName) {
return filePathName.split("/").map((pathItem) => {
if (pathItem.endsWith(".ts")) {
const splitPathItem = pathItem.split(".");
splitPathItem.splice(1, 0, "definition");
return splitPathItem.join(".");
}

return pathItem;
}).join("/");
}

const checkIsUnresolvedTypeScriptType = function(type) {
/**
* Due to TypeScript types being PascalCase, and all other default
* types being lowercase, we determine if this is a typescript type by checking
* the first letter.
*/
return type[0] === type[0].toUpperCase() && isNaN(type[0]);
}

const resolveTypeToValues = function(CEM, type) {
let values = "";

CEM.modules.forEach((cemModule) => {
if (cemModule.kind === "javascript-module") {
cemModule.declarations.forEach((declaration) => {
if (declaration.name === type) {
const sanitizedType = declaration.type?.text;
const matches = sanitizedType
.match(/((?:.*):(?:.*))/gm)
.map((match) => {
return match.match(/(?:(?:')(.*)(?:')|(\d+))/)[0];
});
values = matches.reduce((accum, match) => {
return `${accum}${accum === "" ? "" : " | "}${match}`;
}, values)
}
})
}
});

return values;
}

/**
* @return {import('@custom-elements-manifest/analyzer').Plugin}
*
* This plugin adds the tagName after the manifest has been processed
* See: https://github.com/webcomponents/custom-elements-manifest/blob/main/schema.json
*/
export function tagNameFix() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just so I understand, we need this plugin because we dynamically prefix our component names? And then this goes thru and yoinks the name from the comment. What is the tagName set to before this runs?

return {
name: "fluentTagName",
packageLinkPhase({customElementsManifest, context}){
customElementsManifest.modules.map((item) => {
item.declarations.forEach((declaration) => {
if (declaration.customElement) {
const name = getTagNameFromCommentInDefinitionFile(
resolveDefinitionFilePath(item.path)
);

if (typeof name === "undefined") {
console.error(`no tag name for ${item.path}`);
} else {
declaration.tagName = name;
}
}
})
});
},
};
}

/**
* @return {import('@custom-elements-manifest/analyzer').Plugin}
*
* This plugin changes the types to use pipe syntax so that the vscode plugin can
* correctly interpret the possible values, eg.
* from:
* {
* "name": "heading-level",
* "type": {
* "text": "HeadingLevel"
* },
* "fieldName": "headinglevel"
* }
*
* to:
* {
* "name": "heading-level",
* "type": {
* "text": "1 | 2 | 3 | 4 | 5 | 6"
* },
* "fieldName": "headinglevel"
* },
*/
export function typescriptTypeTextSanitize() {
return {
name: "typescriptTypeTextSanitize",
packageLinkPhase({customElementsManifest, context}){
customElementsManifest.modules.map((item) => {
item.declarations.forEach((declaration) => {
if (declaration.customElement && Array.isArray(declaration.attributes)) {
declaration.attributes.forEach((attribute) => {
if (typeof attribute.type?.text === "string") {
const possibleTypes = attribute.type.text.split("|").map((item) => {
return item.trim();
}).map((possibleType) => {
if (checkIsUnresolvedTypeScriptType(possibleType)) {
return resolveTypeToValues(customElementsManifest, possibleType);
}

return possibleType;
}).join(" | ");

attribute.type.text = possibleTypes;
}
});
}
})
});
},
};
}
11 changes: 8 additions & 3 deletions packages/web-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"dist/dts/",
"dist/esm/",
"dist/*.js",
"dist/*.d.ts"
"dist/*.d.ts",
"dist/*.json"
],
"exports": {
".": {
Expand Down Expand Up @@ -182,6 +183,7 @@
"types": "./dist/dts/utils/index.d.ts",
"default": "./dist/esm/utils/index.js"
},
"./custom-elements.json": "./dist/custom-elements.json",
"./package.json": "./package.json"
},
"sideEffects": [
Expand Down Expand Up @@ -227,11 +229,12 @@
"verify-packaging": "node ./scripts/verify-packaging",
"type-check": "node ./scripts/type-check",
"benchmark": "yarn clean && yarn compile:benchmark && yarn compile && node ./scripts/run-benchmarks",
"cem-analyze": "cem analyze",
"compile": "node ./scripts/compile",
"compile:benchmark": "rollup -c rollup.bench.js",
"clean": "node ./scripts/clean dist",
"generate-api": "api-extractor run --local",
"build": "yarn compile && yarn rollup -c && yarn generate-api",
"build": "yarn compile && yarn rollup -c && yarn generate-api && yarn cem-analyze",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"format": "prettier -w src/**/*.{ts,html} --ignore-path ../../.prettierignore",
Expand All @@ -246,9 +249,11 @@
"devDependencies": {
"@microsoft/fast-element": "2.0.0-beta.26",
"@tensile-perf/web-components": "~0.2.0",
"@custom-elements-manifest/analyzer": "9.8.0",
"@types/web": "^0.0.142",
"@storybook/html": "6.5.15",
"chromedriver": "^125.0.0"
"chromedriver": "^125.0.0",
"custom-element-vs-code-integration": "^1.4.0"
},
"dependencies": {
"@microsoft/fast-web-utilities": "^6.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/web-components/src/avatar/avatar.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { template } from './avatar.template.js';
*
* @public
* @remarks
* HTML Element: \<fluent-badge\>
* HTML Element: \<fluent-avatar\>
*/
export const definition = Avatar.compose({
name: `${FluentDesignSystem.prefix}-avatar`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { template } from './compound-button.template.js';
/**
* @public
* @remarks
* HTML Element: \<fluent-comopund-button\>
* HTML Element: \<fluent-compound-button\>
*/
export const definition = CompoundButton.compose({
name: `${FluentDesignSystem.prefix}-compound-button`,
Expand Down
6 changes: 4 additions & 2 deletions packages/web-components/src/divider/divider.options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Orientation } from '@microsoft/fast-web-utilities';
import type { ValuesOf } from '../utils/index.js';

/**
Expand Down Expand Up @@ -27,7 +26,10 @@ export type DividerRole = ValuesOf<typeof DividerRole>;
* Divider orientation
* @public
*/
export const DividerOrientation = Orientation;
export const DividerOrientation = {
horizontal: 'horizontal',
vertical: 'vertical',
} as const;

/**
* The types for Divider orientation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { template } from './drawer-body.template.js';
*
* @public
* @remarks
* HTML Element: <fluent-drawer>
* HTML Element: <fluent-drawer-body>
*/

export const definition = DrawerBody.compose({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { template } from './menu-button.template.js';
/**
* @public
* @remarks
* HTML Element: \<fluent-button\>
* HTML Element: \<fluent-menu-button\>
*/
export const definition = MenuButton.compose({
name: `${FluentDesignSystem.prefix}-menu-button`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Orientation } from '@microsoft/fast-web-utilities';
import type { ValuesOf } from '../utils/index.js';

/**
* Radio Group orientation
* @public
*/
export const RadioGroupOrientation = Orientation;
export const RadioGroupOrientation = {
horizontal: 'horizontal',
vertical: 'vertical',
} as const;

/**
* Types of Radio Group orientation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { TabPanel } from './tab-panel.js';
import { template } from './tab-panel.template.js';
import { styles } from './tab-panel.styles.js';

/**
* The definition for the Fluent Tab Panel component.
*
* @public
* @remarks
* HTML Element: `<fluent-tab-panel>`
*/
export const definition = TabPanel.compose({
name: `${FluentDesignSystem.prefix}-tab-panel`,
template,
Expand Down
7 changes: 7 additions & 0 deletions packages/web-components/src/tab/tab.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { Tab } from './tab.js';
import { template } from './tab.template.js';
import { styles } from './tab.styles.js';

/**
* The definition for the Fluent Tab component.
*
* @public
* @remarks
* HTML Element: `<fluent-tab>`
*/
export const definition = Tab.compose({
name: `${FluentDesignSystem.prefix}-tab`,
template,
Expand Down
7 changes: 7 additions & 0 deletions packages/web-components/src/tabs/tabs.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { Tabs } from './tabs.js';
import { template } from './tabs.template.js';
import { styles } from './tabs.styles.js';

/**
* The definition for the Fluent Tabs component.
*
* @public
* @remarks
* HTML Element: `<fluent-tabs>`
*/
export const definition = Tabs.compose({
name: `${FluentDesignSystem.prefix}-tabs`,
template,
Expand Down
Loading
Loading