diff --git a/.env.example b/.env.example index fdaa310c..d68dcd63 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ NEXT_PUBLIC_CONTENTSTACK_API_KEY='apikey' NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN='token' -NEXT_PUBLIC_GOOGLE_ANALYTICS='' \ No newline at end of file +NEXT_PUBLIC_GOOGLE_ANALYTICS='' +MDB_USER="" +MDB_PASSWORD="" +FIGMA_TOKEN="" diff --git a/components/ContentstackRichText/ExampleCardBlock/types.ts b/components/ContentstackRichText/ExampleCardBlock/types.ts index 365b0811..5785a1e3 100644 --- a/components/ContentstackRichText/ExampleCardBlock/types.ts +++ b/components/ContentstackRichText/ExampleCardBlock/types.ts @@ -7,6 +7,6 @@ const Variant = { Do: 'do', } as const; -type Variant = typeof Variant[keyof typeof Variant]; +type Variant = (typeof Variant)[keyof typeof Variant]; export { Variant }; diff --git a/components/ContentstackRichText/types.ts b/components/ContentstackRichText/types.ts index 663be039..b38f0b91 100644 --- a/components/ContentstackRichText/types.ts +++ b/components/ContentstackRichText/types.ts @@ -135,7 +135,7 @@ export interface HorizontalLayoutBlockProps { column_1: CSNode; // richText column_2: CSNode; // richText, vertical_align: 'start' | 'center' | 'end' | 'baseline'; - flex_ratio: `${number}:${number}` + flex_ratio: `${number}:${number}`; } export interface BlockPropsMap { diff --git a/components/ContentstackRichText/utils.ts b/components/ContentstackRichText/utils.ts index f7e29034..2080cb5a 100644 --- a/components/ContentstackRichText/utils.ts +++ b/components/ContentstackRichText/utils.ts @@ -22,8 +22,8 @@ export const getCSNodeTextContent = (node?: CSNode): string => { export const nodeHasAssets = (node: CSNode): boolean => { if (['asset', 'entry', 'reference'].includes(node.type)) { - return true + return true; } else { - return node.children && node.children.some(child => nodeHasAssets(child)) + return node.children && node.children.some(child => nodeHasAssets(child)); } -} \ No newline at end of file +}; diff --git a/components/Grid.tsx b/components/Grid.tsx index e3298614..88ef619c 100644 --- a/components/Grid.tsx +++ b/components/Grid.tsx @@ -32,10 +32,10 @@ const Wrap = { WrapReverse: 'wrap-reverse', }; -type Direction = typeof Direction[keyof typeof Direction]; -type Align = typeof Align[keyof typeof Align]; -type Justify = typeof Justify[keyof typeof Justify]; -type Wrap = typeof Wrap[keyof typeof Wrap]; +type Direction = (typeof Direction)[keyof typeof Direction]; +type Align = (typeof Align)[keyof typeof Align]; +type Justify = (typeof Justify)[keyof typeof Justify]; +type Wrap = (typeof Wrap)[keyof typeof Wrap]; type GridContainerProps = JSX.IntrinsicElements['div'] & { direction?: Direction; diff --git a/components/pages/changelogs/FigmaChangelogs.tsx b/components/pages/changelogs/FigmaChangelogs.tsx new file mode 100644 index 00000000..5aac4604 --- /dev/null +++ b/components/pages/changelogs/FigmaChangelogs.tsx @@ -0,0 +1,87 @@ +import { startCase } from 'lodash'; + +import FigmaIcon from 'components/icons/FigmaIcon'; +import ReactIcon from 'components/icons/ReactIcon'; + +import { palette } from '@leafygreen-ui/palette'; +import { spacing } from '@leafygreen-ui/tokens'; +import { + Body, + Disclaimer, + H3, + Link, + Subtitle, +} from '@leafygreen-ui/typography'; + +import { css } from '@emotion/css'; + +const FigmaChangelogs = ({ componentName, figmaEntries }) => { + return ( + <> + {figmaEntries && + figmaEntries.map(figmaVersion => ( +
+ + {new Date(figmaVersion.created_at).toDateString()} + +

{figmaVersion.version}

+ + {startCase(figmaVersion.update_type?.toLowerCase())} + + + {figmaVersion.description} + +
+ {figmaVersion.figma_url && ( + + + Figma Version v{figmaVersion.version} + + )} + {figmaVersion.react_version && ( + + + React Version v{figmaVersion.react_version} + + )} +
+
+ ))} + + ); +}; + +export default FigmaChangelogs; diff --git a/components/pages/changelogs/LogsControl.tsx b/components/pages/changelogs/LogsControl.tsx new file mode 100644 index 00000000..8b5ebe3c --- /dev/null +++ b/components/pages/changelogs/LogsControl.tsx @@ -0,0 +1,47 @@ +import FigmaIcon from 'components/icons/FigmaIcon'; +import ReactIcon from 'components/icons/ReactIcon'; + +import { css } from '@leafygreen-ui/emotion'; +import { + SegmentedControl, + SegmentedControlOption, +} from '@leafygreen-ui/segmented-control'; +import { spacing } from '@leafygreen-ui/tokens'; + +const controlOptionStyles = css` + display: flex; + align-items: center; +`; + +const controlOptionTextStyles = css` + margin-left: ${spacing[1]}px; +`; + +const LogsControl = ({ setDisplayedLogs, figmaEntries, reactVersion }) => { + return ( +
+ + {figmaEntries && ( + +
+ + + Figma - v{figmaEntries[0].version} + +
+
+ )} + +
+ + + React - v{reactVersion} + +
+
+
+
+ ); +}; + +export default LogsControl; diff --git a/components/pages/changelogs/ReactChangelogs.tsx b/components/pages/changelogs/ReactChangelogs.tsx new file mode 100644 index 00000000..8ee1da74 --- /dev/null +++ b/components/pages/changelogs/ReactChangelogs.tsx @@ -0,0 +1,30 @@ +import { palette } from '@leafygreen-ui/palette'; +import { spacing } from '@leafygreen-ui/tokens'; + +import { css } from '@emotion/css'; + +const changelogStyles = css` + color: ${palette.gray.dark3}; + pointer-events: none; + + & > h2 { + padding-top: ${spacing[3]}px; + border-top: 1px solid ${palette.gray.light2}; + } + + a { + color: ${palette.gray.dark3}; + text-decoration: none; + } +`; + +const ReactChangelogs = ({ reactChangelogs }) => { + return ( +
+ ); +}; + +export default ReactChangelogs; diff --git a/components/pages/documentation/CodeDocs.tsx b/components/pages/documentation/CodeDocs.tsx index da560e07..d995b42c 100644 --- a/components/pages/documentation/CodeDocs.tsx +++ b/components/pages/documentation/CodeDocs.tsx @@ -9,7 +9,6 @@ import { InstallInstructions } from './InstallInstructions'; function CodeDocs({ componentKebabCaseName, - changelog, tsDoc, }: BaseLayoutProps & { tsDoc?: Array }) { const tsDocArray = tsDoc?.sort( @@ -30,10 +29,7 @@ function CodeDocs({ return ( <> - + {tsDocArray && tsDocArray.length > 0 ? ( ) : ( diff --git a/components/pages/documentation/InstallInstructions.tsx b/components/pages/documentation/InstallInstructions.tsx index bcf79e44..bda4bd7b 100644 --- a/components/pages/documentation/InstallInstructions.tsx +++ b/components/pages/documentation/InstallInstructions.tsx @@ -1,13 +1,9 @@ import { useState } from 'react'; -import Button from '@leafygreen-ui/button'; import Card from '@leafygreen-ui/card'; import Copyable from '@leafygreen-ui/copyable'; import { css, cx } from '@leafygreen-ui/emotion'; import { useViewportSize } from '@leafygreen-ui/hooks'; -import ActivityFeedIcon from '@leafygreen-ui/icon/dist/ActivityFeed'; -import Modal from '@leafygreen-ui/modal'; -import { palette } from '@leafygreen-ui/palette'; import { SegmentedControl, SegmentedControlOption, @@ -30,68 +26,7 @@ const subtitlePadding = css` padding-bottom: ${spacing[3]}px; `; -const changelogStyles = css` - color: ${palette.gray.dark3}; - pointer-events: none; - - & > h2 { - padding-top: ${spacing[3]}px; - border-top: 1px solid ${palette.gray.light2}; - } - - a { - color: ${palette.gray.dark3}; - text-decoration: none; - } -`; - -interface VersionCardProps { - version?: string; - changelog: string; - isMobile?: boolean; -} -function VersionCard({ - version, - changelog, - isMobile = false, -}: VersionCardProps) { - const [openModal, setOpenModal] = useState(false); - - return ( - - - Version {version} - - - -
-
-
- ); -} - -export const InstallInstructions = ({ componentKebabCaseName, changelog }) => { - const version = changelog?.split('h2')[1]?.replace(/[>/<]+/g, ''); +export const InstallInstructions = ({ componentKebabCaseName }) => { const viewport = useViewportSize(); const isMobile = viewport?.width ? viewport?.width < breakpoints.Tablet @@ -138,8 +73,6 @@ export const InstallInstructions = ({ componentKebabCaseName, changelog }) => { - - ); }; diff --git a/components/pages/example/Knob/types.ts b/components/pages/example/Knob/types.ts index 786bd76a..31e82196 100644 --- a/components/pages/example/Knob/types.ts +++ b/components/pages/example/Knob/types.ts @@ -9,5 +9,5 @@ export interface KnobProps extends HTMLElementProps<'input'> { value: any; onChange: (val: any) => void; darkMode?: boolean; - [key: string]: any + [key: string]: any; } diff --git a/components/pages/example/types.ts b/components/pages/example/types.ts index 87fb1645..7a74f489 100644 --- a/components/pages/example/types.ts +++ b/components/pages/example/types.ts @@ -39,8 +39,8 @@ export type KnobType = PropItem & { max?: number; step?: number; disabled?: boolean; - [key: string]: any - } + [key: string]: any; + }; }; export interface LiveExampleState { diff --git a/components/pages/example/utils.ts b/components/pages/example/utils.ts index e3b91965..e6a81478 100644 --- a/components/pages/example/utils.ts +++ b/components/pages/example/utils.ts @@ -129,9 +129,9 @@ function getPropItemToKnobTypeMapFn({ StoryFn, TSDocProp, }), - args: getOtherControlArgs({meta, StoryFn, TSDocProp}) - } as KnobType - } + args: getOtherControlArgs({ meta, StoryFn, TSDocProp }), + } as KnobType; + }; } /** @@ -233,9 +233,13 @@ function getOtherControlArgs({ }: MetadataSources): object | undefined { const SBInputType = getSBInputType({ meta, StoryFn, TSDocProp }); - if (SBInputType && SBInputType.control && typeof SBInputType.control === 'object') { - const {type, ...args} = SBInputType.control - return args + if ( + SBInputType && + SBInputType.control && + typeof SBInputType.control === 'object' + ) { + const { type, ...args } = SBInputType.control; + return args; } } diff --git a/layouts/ComponentLayout.tsx b/layouts/ComponentLayout.tsx index ee651064..7a8aa604 100644 --- a/layouts/ComponentLayout.tsx +++ b/layouts/ComponentLayout.tsx @@ -119,6 +119,8 @@ const ComponentLinks = ({ ); +const TABS = ['example', 'guidelines', 'documentation', 'changelogs']; + function ComponentLayout({ component, children, @@ -137,9 +139,7 @@ function ComponentLayout({ React.useEffect(() => { const activeTab = router.pathname.split('/').filter(subStr => !!subStr)[2]; - setSelected( - activeTab === 'example' ? 0 : activeTab === 'guidelines' ? 1 : 2, - ); + setSelected(TABS.indexOf(activeTab)); }, [router]); return ( @@ -202,6 +202,12 @@ function ComponentLayout({ >
{children}
+ +
{children}
+
diff --git a/package.json b/package.json index 4edbb140..9e4cabef 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "fix:eslint": "eslint . --ext js,jsx,ts,tsx,md,mdx --fix", "fix": "npm-run-all --parallel fix:*", "check:ts": "tsc --project tsconfig.json", - "ts-node": "npx ts-node -P './tsconfig.json' -O '{\"module\": \"commonjs\"}' --", + "ts-node": "NODE_PATH=./ npx ts-node -P './tsconfig.json' -O '{\"module\": \"commonjs\"}' --", "update-packages": "yarn ts-node ./scripts/update-packages.ts" }, "dependencies": { @@ -83,9 +83,11 @@ "@svgr/webpack": "^6.3.1", "@types/node": "^18.0.6", "@types/react": "^18.0.21", + "axios": "^1.3.4", "clipboard": "^2.0.6", "commander": "^9.4.1", "contentstack": "^3.15.6", + "cors": "^2.8.5", "eslint": "^8.23.1", "eslint-config-prettier": "8.5.0", "eslint-plugin-import": "2.26.0", @@ -93,6 +95,7 @@ "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "gray-matter": "4.0.3", + "mongodb": "^5.1.0", "next": "^12.3.1", "polished": "^4.1.3", "prettier": "^2.7.1", diff --git a/pages/.DS_Store b/pages/.DS_Store index 9d1d4c5e..dfa7bea3 100644 Binary files a/pages/.DS_Store and b/pages/.DS_Store differ diff --git a/pages/api/figma-publish.ts b/pages/api/figma-publish.ts new file mode 100644 index 00000000..c8fa67b9 --- /dev/null +++ b/pages/api/figma-publish.ts @@ -0,0 +1,103 @@ +import axios from 'axios'; +import { ObjectId } from 'mongodb'; + +import { calcNewVersion } from '../../utils/Figma/calcNewVersion'; +import { LibraryPublishEvent } from '../../utils/Figma/figma.types'; +import { getFigmaVersionHistory } from '../../utils/Figma/getFigmaVersionHistory'; +import { parseUpdatesFromFigmaDescription } from '../../utils/Figma/parseDescription'; +import { connectToFigmaVersionsCollection } from '../../utils/MongoDB/connect'; +import { getLatestEntries } from '../../utils/MongoDB/getLatestEntries'; + +const WEBHOOK_ID = '494792'; +// const FILENAME = 'LeafyGreen Design System' +const FILENAME = 'Skunkworks Test DS'; + +export default async function handleFigmaPublish( + req: { + method: 'POST' | 'GET' | 'PUT'; + body: LibraryPublishEvent; + }, + res, +) { + if (req.method === 'POST') { + const body = req.body; + const updatedFile = body.file_name; + const requestWebhookId = body.webhook_id; + + const figmaUpdateList = parseUpdatesFromFigmaDescription(body.description); + + if ( + updatedFile !== FILENAME || + requestWebhookId !== WEBHOOK_ID || + !figmaUpdateList + ) { + res.status(403).error('No updates found.'); + return; + } + + // 1. GET the URL to the _second last_ publish from Figma's version history API + // (the _last_ publish is the current one) + const versionHistory = await getFigmaVersionHistory(body); + + if (!versionHistory) { + res.status(403).error('Could not access Figma'); + return; + } + + const { + versions: [currentVersion], + getVersionUrl, + } = versionHistory; + const currVersionUrl = getVersionUrl(currentVersion); + + const { collection } = await connectToFigmaVersionsCollection(); + const entries = await getLatestEntries({ + collection, + updates: figmaUpdateList, + }).toArray(); + + // For each updated component: + figmaUpdateList.forEach(update => { + // The latest entry on MDB + const entryGroup = entries.find(e => e._id === update.component); + + // Calculate the new version based on the last FigmaVersion + // and whether `versionUpdate` is a PATCH, MINOR, or MAJOR + // if there is no `doc`, version is set to 1.0.0 + const { major, minor, patch, version } = calcNewVersion({ + update, + doc: entryGroup?.latest, + }); + + // POST a new entry to MDB with the new version, Component, and description + collection.insertOne({ + _id: new ObjectId(), + component: update.component, + update_type: update.type, + description: update.description, + created_at: new Date().toString(), + major, + minor, + patch, + version, + figma_url: currVersionUrl?.href, + }); + }); + + // send status code 200 + res.status(200); + } else if (req.method === 'GET') { + // get latest figma publish + const figmaWebhooks = await axios.get( + `https://api.figma.com/v2/webhooks/${WEBHOOK_ID}/requests`, + { + headers: { + 'X-Figma-Token': process.env.FIGMA_TOKEN, + }, + }, + ); + res.status(200).json(figmaWebhooks.data); + } else { + res.status(404).error('Incorrect request method.'); + } +} diff --git a/pages/component/[componentName]/changelogs.tsx b/pages/component/[componentName]/changelogs.tsx new file mode 100644 index 00000000..95362aad --- /dev/null +++ b/pages/component/[componentName]/changelogs.tsx @@ -0,0 +1,103 @@ +import { ReactElement, useMemo, useState } from 'react'; +import ComponentLayout from 'layouts/ComponentLayout'; +import { startCase } from 'lodash'; +import { containerPadding } from 'styles/globals'; +import { getChangelog } from 'utils/_getComponentResources'; +import { getComponent } from 'utils/ContentStack/getContentstackResources'; +import { getStaticComponentPaths } from 'utils/ContentStack/getStaticComponent'; +import { FigmaVersionsMDBDocument } from 'utils/Figma/figma.types'; +import { getComponentEntriesArray } from 'utils/MongoDB/getComponentEntries'; + +import FigmaChangelogs from 'components/pages/changelogs/FigmaChangelogs'; +import LogsControl from 'components/pages/changelogs/LogsControl'; +import ReactChangelogs from 'components/pages/changelogs/ReactChangelogs'; + +import { spacing } from '@leafygreen-ui/tokens'; + +import { css, cx } from '@emotion/css'; + +interface DocsPageProps { + componentName: string; + reactChangelogs: string; + reactVersion: string; + figmaEntries: string; +} + +const ComponentChangelogs = ({ + componentName, + reactChangelogs, + reactVersion, + figmaEntries: figmaEntriesString, +}: DocsPageProps) => { + const figmaEntries: Array = useMemo( + () => JSON.parse(figmaEntriesString), + [figmaEntriesString], + ); + + const [displayedLogs, setDisplayedLogs] = useState('figma'); + return ( +
+ +
+ {displayedLogs === 'figma' ? ( + + ) : ( + + )} +
+
+ ); +}; + +ComponentChangelogs.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export const getStaticPaths = getStaticComponentPaths; + +export async function getStaticProps({ params: { componentName } }) { + const reactChangelogs = await getChangelog(componentName); + const reactVersion = reactChangelogs?.split('h2')[1]?.replace(/[>/<]+/g, ''); + + const component = await getComponent(componentName, { + includeContent: false, + }); + + const figmaEntries = ( + await getComponentEntriesArray({ + component: startCase(componentName), + }) + ).map(({ _id, major, minor, patch, ...rest }) => ({ ...rest })); + + return { + props: { + componentName, + component, + reactChangelogs, + reactVersion, + figmaEntries: JSON.stringify(figmaEntries), + }, + }; +} + +export default ComponentChangelogs; diff --git a/pages/component/[componentName]/documentation.tsx b/pages/component/[componentName]/documentation.tsx index d98ce845..c727c0b1 100644 --- a/pages/component/[componentName]/documentation.tsx +++ b/pages/component/[componentName]/documentation.tsx @@ -14,14 +14,12 @@ import { css, cx } from '@emotion/css'; interface DocsPageProps { componentName: string; - changelog: string; readme: string; tsDoc: Array; } const ComponentDocumentation = ({ componentName, - changelog, readme, tsDoc, }: DocsPageProps) => { @@ -37,7 +35,6 @@ const ComponentDocumentation = ({ @@ -55,13 +52,13 @@ export const getStaticPaths = getStaticComponentPaths; export async function getStaticProps({ params: { componentName } }) { const { - props: { changelog, readme, tsDoc }, + props: { readme, tsDoc }, } = await getDependencyDocumentation(componentName); const component = await getComponent(componentName, { includeContent: false, }); - return { props: { componentName, component, changelog, readme, tsDoc } }; + return { props: { componentName, component, readme, tsDoc } }; } export default ComponentDocumentation; diff --git a/tsconfig.json b/tsconfig.json index 8eeae377..07ffb131 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,8 +26,8 @@ "**/*.ts", "**/*.tsx", "components/**/*.tsx", - "utils/*.tsx", - "pages/*.tsx" + "utils/**/*.tsx", + "pages/**/*.tsx" ], "exclude": ["node_modules", "deprecated"] } diff --git a/utils/ContentStack/FigmaVersion.ts b/utils/ContentStack/FigmaVersion.ts new file mode 100644 index 00000000..1dd7d795 --- /dev/null +++ b/utils/ContentStack/FigmaVersion.ts @@ -0,0 +1,81 @@ +import { Stack } from './Stack'; + +interface FigmaVersionField { + uid: string; + title: string; + react_version: string; + figma_link: string; + updated_at: string; +} + +function parseVersionString(versionString: string): Array | undefined { + return versionString.match(/[0-9]+/g)?.map(str => Number(str)); +} + +function sortVersions(a: FigmaVersionField, z: FigmaVersionField) { + const aVersion = parseVersionString(a.title); + const zVersion = parseVersionString(z.title); + + if (aVersion?.length === 3 && zVersion?.length === 3) { + const [aMaj, aMin, aPatch] = aVersion; + const [zMaj, zMin, zPatch] = zVersion; + + // if the major versions are the same + if (aMaj === zMaj) { + if (aMin === zMin) { + if (aPatch === zPatch) { + console.error('Two FigmaVersion entries have the same version', a, z); + return 0; // the entire versions are the same // error + } + + return aPatch > zPatch ? -1 : 1; + } + + return aMin > zMin ? -1 : 1; + } + + return aMaj > zMaj ? -1 : 1; + } + + return 0; +} + +/** + * @returns all FigmaVersion entries for given component + * @deprecated + */ +export async function getComponentFigmaVersions( + componentUid: string, +): Promise | undefined> { + try { + const query = Stack.ContentType('figma_version').Query(); + const result: Array> = await query + .where('component.uid', componentUid) + .toJSON() + .find(); + return result[0].sort(sortVersions); + } catch (error) { + console.error('Component figma versions not found', error); + } +} + +/** + * @returns the last FigmaVersion for the given component + * @deprecated + */ +export async function getLastComponentFigmaVersion( + componentUid: string, +): Promise { + try { + const query = Stack.ContentType('figma_version').Query(); + const result = await query + .where('component.uid', componentUid) + .descending('version') + .limit(1) + .toJSON() + .find(); + return result[0]; + } catch (error) { + console.error('Component figma versions not found', error); + } +} diff --git a/utils/ContentStack/Stack.ts b/utils/ContentStack/Stack.ts new file mode 100644 index 00000000..93d30702 --- /dev/null +++ b/utils/ContentStack/Stack.ts @@ -0,0 +1,6 @@ +import Contentstack from 'contentstack'; +export const Stack = Contentstack.Stack({ + api_key: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY as string, + delivery_token: process.env.NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN as string, + environment: 'main', +}); diff --git a/utils/ContentStack/getContentstackResources.ts b/utils/ContentStack/getContentstackResources.ts index b13fd9cd..39d774a0 100644 --- a/utils/ContentStack/getContentstackResources.ts +++ b/utils/ContentStack/getContentstackResources.ts @@ -66,6 +66,7 @@ export async function getComponent( ]) .toJSON() .find(); + return result[0][0]; } catch (error) { console.error('Component page not found', error); diff --git a/utils/ContentStack/types.ts b/utils/ContentStack/types.ts index 91abbf35..4292e7d2 100644 --- a/utils/ContentStack/types.ts +++ b/utils/ContentStack/types.ts @@ -44,6 +44,5 @@ export interface ComponentFields extends ComponentPageMeta { export interface BaseLayoutProps { componentName: string; componentKebabCaseName: string; - changelog: string; readme: string; } diff --git a/utils/Figma/calcNewVersion.ts b/utils/Figma/calcNewVersion.ts new file mode 100644 index 00000000..940f8c5f --- /dev/null +++ b/utils/Figma/calcNewVersion.ts @@ -0,0 +1,18 @@ +import { FigmaComponentUpdate, FigmaVersionsMDBDocument } from './figma.types'; + +export function calcNewVersion({ + update, + doc, +}: { + update: FigmaComponentUpdate; + doc?: FigmaVersionsMDBDocument; +}) { + let { major, minor, patch } = doc || { major: 1, minor: 0, patch: 0 }; + if (update?.type === 'MAJOR') major++; + else if (update?.type === 'MINOR') minor++; + else if (update?.type === 'PATCH') patch++; + + const version = `${major}.${minor}.${patch}`; + + return { version, major, minor, patch }; +} diff --git a/utils/Figma/figma.types.ts b/utils/Figma/figma.types.ts new file mode 100644 index 00000000..20ca5f3f --- /dev/null +++ b/utils/Figma/figma.types.ts @@ -0,0 +1,77 @@ +import { Document, ObjectId } from 'mongodb'; +export interface LibraryPublishEvent { + created_components: Array; + created_styles: Array; + deleted_components: Array; + deleted_styles: Array; + description: string; + event_type: 'LIBRARY_PUBLISH'; + file_key: string; + file_name: string; + modified_components: Array; + modified_styles: Array; + passcode: string; + timestamp: string; + triggered_by: { + id: string; + handle: string; + }; + webhook_id: string; +} + +export interface FigmaVersionEvent { + created_at: string; + description: string; + id: string; + label: string; + thumbnail_url: string; + user: { + id: string; + handle: string; + }; +} +export interface FigmaComponentUpdate { + type?: 'PATCH' | 'MINOR' | 'MAJOR'; + component: string; + description?: string; +} + +export interface FigmaVersionsMDBDocument extends Document { + _id: ObjectId; + + /** + * The component name + */ + component: string; + version: string; + major: number; + minor: number; + patch: number; + + /** + * What type of update this was + */ + update_type?: FigmaComponentUpdate['type']; + + /** + * When the MDB document was created + * + * @type Date.toString(); + */ + created_at: string; + + /** + * Update description + */ + description?: string; + + /** + * A direct link to the Figma version + */ + figma_url?: string; + + /** + * The associated React component version + */ + react_version?: string; +} diff --git a/utils/Figma/getFigmaVersionHistory.ts b/utils/Figma/getFigmaVersionHistory.ts new file mode 100644 index 00000000..3a0cc75a --- /dev/null +++ b/utils/Figma/getFigmaVersionHistory.ts @@ -0,0 +1,33 @@ +import { isUndefined } from 'lodash'; +import { + FigmaVersionEvent, + LibraryPublishEvent, +} from 'utils/Figma/figma.types'; + +/** + * Retrieves the versionn history for a given figma file + */ +export async function getFigmaVersionHistory(requestBody: LibraryPublishEvent) { + if (isUndefined(process.env.FIGMA_TOKEN)) { + console.error('Figma token not found'); + return; + } + + const { versions } = await fetch( + `https://api.figma.com/v1/files/${requestBody.file_key}/versions`, + { + headers: { + 'X-Figma-Token': process.env.FIGMA_TOKEN, + }, + }, + ).then(data => data.json()); + + return { + versions: versions as Array, + getVersionUrl: (version: FigmaVersionEvent | undefined) => + version && + new URL( + `https://www.figma.com/file/${requestBody.file_key}/${requestBody.file_name}?version-id=${version.id}`, + ), + }; +} diff --git a/utils/Figma/parseDescription.ts b/utils/Figma/parseDescription.ts new file mode 100644 index 00000000..92505445 --- /dev/null +++ b/utils/Figma/parseDescription.ts @@ -0,0 +1,28 @@ +import { FigmaComponentUpdate } from './figma.types'; + +/** + * Convert the formatted update string to an array of update objects. + * + * Expected `desctiption` format: + * `[PATCH] - Update text ... + * [MINOR] - Update text ... + * ...etc` + */ +export function parseUpdatesFromFigmaDescription( + description: string, +): Array | undefined { + const updateLines = description.match(/\[[A-Z]*\](.+)/g); + + return updateLines?.map(updateStr => { + // E.g. [MINOR] Banner - something has changed within me + const type = updateStr.match(/(?<=\[)[A-Z]*(?=\])/g)?.[0].trim(); // MINOR + const component = updateStr.match(/(?<=\])(.*)(?=-)/g)?.[0].trim(); // Banner + const description = updateStr.match(/(?<=-)(.*)/g)?.[0].trim(); // something has changed within me + + return { + type, + component, + description, + } as FigmaComponentUpdate; + }); +} diff --git a/utils/MongoDB/connect.ts b/utils/MongoDB/connect.ts new file mode 100644 index 00000000..71cf29ce --- /dev/null +++ b/utils/MongoDB/connect.ts @@ -0,0 +1,22 @@ +import { MongoClient, ServerApiVersion } from 'mongodb'; + +import type { FigmaVersionsMDBDocument } from './mongodb.types'; + +const uri = `mongodb+srv://${process.env.MDB_USER}:${process.env.MDB_PASSWORD}@figmaversions.fctbuvl.mongodb.net/?retryWrites=true&w=majority`; + +export const MDBClient = new MongoClient(uri, { + serverApi: ServerApiVersion.v1, +}); + +export async function connectToFigmaVersionsCollection() { + await MDBClient.connect(); + const collection = + MDBClient.db('FigmaVersions').collection( + 'versions', + ); + + return { + collection, + close: () => MDBClient.close(), + }; +} diff --git a/utils/MongoDB/getComponentEntries.ts b/utils/MongoDB/getComponentEntries.ts new file mode 100644 index 00000000..026fd736 --- /dev/null +++ b/utils/MongoDB/getComponentEntries.ts @@ -0,0 +1,31 @@ +import { connectToFigmaVersionsCollection } from './connect'; +import type { FigmaVersionsMDBDocument } from './mongodb.types'; + +/** + * Retrieves version entries for a given component + */ +export async function getComponentEntriesArray({ + component, + limit = 10, +}: { + component: FigmaVersionsMDBDocument['component']; + limit?: number; +}) { + const { collection, close: closeDB } = + await connectToFigmaVersionsCollection(); + + const entries = collection + .find({ + component, + }) + .sort({ + major: -1, + minor: -1, + patch: -1, + }) + .limit(limit); + const entriesArray = await entries.toArray(); + + closeDB(); + return entriesArray; +} diff --git a/utils/MongoDB/getLatestEntries.ts b/utils/MongoDB/getLatestEntries.ts new file mode 100644 index 00000000..f13c7e86 --- /dev/null +++ b/utils/MongoDB/getLatestEntries.ts @@ -0,0 +1,46 @@ +import { Collection } from 'mongodb'; + +import type { + FigmaComponentUpdate, + FigmaVersionsMDBDocument, +} from '../Figma/figma.types'; + +/** + * Retrieves the latest version entry for all updated components + */ +export function getLatestEntries({ + collection, + updates, +}: { + collection: Collection; + updates: Array; +}) { + // Get the latest entries + const entries = collection.aggregate<{ + _id: string; + latest: FigmaVersionsMDBDocument; + }>([ + { + $match: { + component: { + $in: updates.map(u => u.component), + }, + }, + }, + { + $sort: { + major: -1, + minor: -1, + patch: -1, + }, + }, + { + $group: { + _id: '$component', + latest: { $first: '$$ROOT' }, + }, + }, + ]); + + return entries; +} diff --git a/utils/MongoDB/mongodb.types.ts b/utils/MongoDB/mongodb.types.ts new file mode 100644 index 00000000..c9fb5bc4 --- /dev/null +++ b/utils/MongoDB/mongodb.types.ts @@ -0,0 +1,2 @@ +// Re-exports the type +export { type FigmaVersionsMDBDocument } from '../Figma/figma.types'; diff --git a/utils/MongoDB/updateFigmaUrl.ts b/utils/MongoDB/updateFigmaUrl.ts new file mode 100644 index 00000000..e77a6062 --- /dev/null +++ b/utils/MongoDB/updateFigmaUrl.ts @@ -0,0 +1,25 @@ +import { type ObjectId, Collection } from 'mongodb'; + +import { FigmaVersionsMDBDocument } from './mongodb.types'; + +/** + * Updates the `figma_url` field of an entry + */ +export function updateFigmaUrl({ + collection, + id, + url, +}: { + collection: Collection; + id: ObjectId; + url: URL; +}): void { + collection.updateOne( + { _id: id }, + { + $set: { + figma_url: url.href, + }, + }, + ); +} diff --git a/yarn.lock b/yarn.lock index f7138e62..bd8236e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3389,6 +3389,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/webidl-conversions@*": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7" + integrity sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog== + "@types/webpack-env@^1.16.0": version "1.18.0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.0.tgz#ed6ecaa8e5ed5dfe8b2b3d00181702c9925f13fb" @@ -3415,6 +3420,14 @@ anymatch "^3.0.0" source-map "^0.6.0" +"@types/whatwg-url@^8.2.1": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" + integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA== + dependencies: + "@types/node" "*" + "@types/webidl-conversions" "*" + "@typescript-eslint/eslint-plugin@^5.38.0": version "5.53.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.53.0.tgz#24b8b4a952f3c615fe070e3c461dd852b5056734" @@ -4189,6 +4202,15 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== +axios@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" + integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" @@ -4521,6 +4543,11 @@ browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^ node-releases "^2.0.8" update-browserslist-db "^1.0.10" +bson@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-5.0.1.tgz#4cd3eeeabf6652ef0d6ab600f9a18212d39baac3" + integrity sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -5110,6 +5137,14 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -6544,6 +6579,11 @@ focus-trap@^7.3.1: dependencies: tabbable "^6.1.1" +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -6605,6 +6645,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -8637,6 +8686,11 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -9221,6 +9275,25 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mongodb-connection-string-url@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf" + integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ== + dependencies: + "@types/whatwg-url" "^8.2.1" + whatwg-url "^11.0.0" + +mongodb@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.1.0.tgz#e551f9e496777bde9173e51d16c163ab2c805b9d" + integrity sha512-qgKb7y+EI90y4weY3z5+lIgm8wmexbonz0GalHkSElQXVKtRuwqXuhXKccyvIjXCJVy9qPV82zsinY0W1FBnJw== + dependencies: + bson "^5.0.1" + mongodb-connection-string-url "^2.6.0" + socks "^2.7.1" + optionalDependencies: + saslprep "^1.0.3" + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -9484,7 +9557,7 @@ num2fraction@^1.2.2: resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg== -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -10250,6 +10323,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -10302,7 +10380,7 @@ punycode@^1.2.4: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== @@ -10961,6 +11039,13 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saslprep@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" @@ -11210,6 +11295,11 @@ slate@^0.72.0: is-plain-object "^5.0.0" tiny-warning "^1.0.3" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -11240,6 +11330,14 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socks@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -11299,6 +11397,13 @@ space-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== + dependencies: + memory-pager "^1.0.2" + spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -11865,6 +11970,13 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -12387,7 +12499,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== @@ -12487,6 +12599,11 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-dev-middleware@^3.7.3: version "3.7.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" @@ -12634,6 +12751,14 @@ whatwg-fetch@^3.4.1: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"