diff --git a/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/long-read-header.spec.tsx.snap b/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/long-read-header.spec.tsx.snap index d4d980b60e..ec88edbcb8 100644 --- a/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/long-read-header.spec.tsx.snap +++ b/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/long-read-header.spec.tsx.snap @@ -23,7 +23,6 @@ exports[`LongReadHeader should show a full featured long read header 1`] = ` style={ Array [ Object { - "backgroundColor": "rgba(0,0,0,.25)", "width": "100%", }, Object { @@ -84,7 +83,7 @@ exports[`LongReadHeader should show a full featured long read header 1`] = ` "flexShrink": 0, "fontFamily": "GHGuardianHeadline-Regular", "fontSize": 28, - "lineHeight": 30, + "lineHeight": 32, }, Object { "color": "#ffffff", @@ -440,7 +439,7 @@ exports[`LongReadHeader should show long read header without an image or a kicke "flexShrink": 0, "fontFamily": "GHGuardianHeadline-Regular", "fontSize": 28, - "lineHeight": 30, + "lineHeight": 32, }, Object { "color": "#ffffff", diff --git a/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/news-header.spec.tsx.snap b/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/news-header.spec.tsx.snap index bfd55ce4f6..e2e84d5778 100644 --- a/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/news-header.spec.tsx.snap +++ b/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/news-header.spec.tsx.snap @@ -12,59 +12,80 @@ exports[`NewsHeader should show a full featured news header 1`] = ` ] } > - + + "uri": "https://d2cf1ljtg904cv.cloudfront.net/issue/media/phone/media/path/to/image.png", + } + } + style={ + Array [ + Object { + "width": "100%", + }, + Object { + "aspectRatio": 1.5, + "marginBottom": 3, + }, + ] + } + /> + - + - News - + Array [ + Object { + "marginTop": 2.4, + }, + Object { + "color": "#121212", + }, + ], + ] + } + > + News + + - + - + - - - - - - - - - - + + + + + + + + + - Andrew Sparrow - + Array [ + Object { + "color": "#121212", + }, + Object { + "marginBottom": 12, + }, + ], + ] + } + > + Andrew Sparrow + + `; @@ -415,7 +450,13 @@ exports[`NewsHeader should show news header without an image or a kicker 1`] = ` ] } > - + - + - - - - - - - - - - + + + + + + + + + - Andrew Sparrow - + Array [ + Object { + "color": "#121212", + }, + Object { + "marginBottom": 12, + }, + ], + ] + } + > + Andrew Sparrow + + `; diff --git a/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/opinion-header.spec.tsx.snap b/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/opinion-header.spec.tsx.snap index 6ade50751a..2a71dd3f5d 100644 --- a/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/opinion-header.spec.tsx.snap +++ b/projects/Mallard/src/components/article/article-header/__tests__/__snapshots__/opinion-header.spec.tsx.snap @@ -21,26 +21,39 @@ exports[`OpinionHeader should show a full featured opinion header 1`] = ` ] } > - + + + - + > - - - - - - - - - - + > + + + + + + + - + - - - - - - - - - - + > + + + + + + + - + - - This is a test Standfirst - - + This is a test Standfirst + `; exports[`ArticleStandfirst should display a default Standfirst 1`] = ` - - + - - This is a test Standfirst - - + This is a test Standfirst + `; diff --git a/projects/Mallard/src/components/front/collection-page/collection-page.tsx b/projects/Mallard/src/components/front/collection-page/collection-page.tsx index 3dfaf4eb20..d3f8dd9f07 100644 --- a/projects/Mallard/src/components/front/collection-page/collection-page.tsx +++ b/projects/Mallard/src/components/front/collection-page/collection-page.tsx @@ -4,21 +4,18 @@ import { metrics } from 'src/theme/spacing' import { color } from 'src/theme/color' import { Row } from './row' -import { CAPIArticle, Issue, Collection, Front } from 'src/common' -import { PageLayout, useCardBackgroundStyle } from '../helpers' -import { FlexErrorMessage } from 'src/components/layout/ui/errors/flex-error-message' import { - splashPage, - twoStoryPage, - superHeroPage, - threeStoryPage, - fourStoryPage, - fiveStoryPage, - sixStoryPage, -} from '../layouts' -import { PathToArticle } from 'src/screens/article-screen' + CAPIArticle, + Issue, + Collection, + Front, + defaultCardAppearances, + FrontCardAppearance, +} from 'src/common' +import { useCardBackgroundStyle } from '../helpers' +import { FlexErrorMessage } from 'src/components/layout/ui/errors/flex-error-message' +import { layouts } from '../layouts' import { ArticleNavigator } from '../../../screens/article-screen' -import { useArticle } from 'src/hooks/use-article' const styles = StyleSheet.create({ root: { @@ -45,10 +42,31 @@ export interface PropTypes { articleNavigator: ArticleNavigator issue: Issue['key'] front: Front['key'] - pageLayout?: PageLayout + appearance: FrontCardAppearance | null collection: Collection['key'] } +const getPageLayout = ( + appearance: FrontCardAppearance | null, + length: number, +) => { + if (!appearance) { + if ( + length === 1 || + length === 2 || + length === 3 || + length === 4 || + length === 5 || + length === 6 + ) { + return layouts[defaultCardAppearances[length]] + } + return layouts[defaultCardAppearances[6]] + } else { + return layouts[appearance] + } +} + const CollectionPage = ({ articlesInCard, articleNavigator, @@ -56,37 +74,15 @@ const CollectionPage = ({ translate, issue, front, - pageLayout, + appearance, }: { translate: Animated.AnimatedInterpolation } & PropTypes) => { const background = useCardBackgroundStyle() if (!articlesInCard.length) { return } - if (!pageLayout) { - switch (articlesInCard.length) { - // case 1: - // pageLayout = superHeroPage - // break - //THIS IS JUST FOR DEVELOPMENT TODO: Make this layout work from API - case 1: - pageLayout = superHeroPage - break - case 2: - pageLayout = twoStoryPage - break - case 3: - pageLayout = threeStoryPage - break - case 4: - pageLayout = fourStoryPage - break - case 5: - pageLayout = fiveStoryPage - break - default: - pageLayout = sixStoryPage - } - } + + const pageLayout = getPageLayout(appearance, articlesInCard.length) + return ( {pageLayout.map((row, index) => ( diff --git a/projects/Mallard/src/components/front/index.tsx b/projects/Mallard/src/components/front/index.tsx index 09982d48d7..1af455024b 100644 --- a/projects/Mallard/src/components/front/index.tsx +++ b/projects/Mallard/src/components/front/index.tsx @@ -6,8 +6,6 @@ import { Spinner } from '../spinner' import { FlexCenter } from '../layout/flex-center' import { Issue, PillarFromPalette, Front as FrontType } from 'src/common' import { FlexErrorMessage } from '../layout/ui/errors/flex-error-message' -import { GENERIC_ERROR } from 'src/helpers/words' -import { useSettings } from 'src/hooks/use-settings' import { FlatCard, getColor, @@ -26,12 +24,12 @@ import { WithArticle, getAppearancePillar } from '../../hooks/use-article' const CollectionPageInFront = ({ index, - appearance, + pillar, scrollX, ...collectionPageProps }: { index: number - appearance: PillarFromPalette + pillar: PillarFromPalette scrollX: Animated.Value } & PropTypes) => { const { width } = Dimensions.get('window') @@ -49,7 +47,7 @@ const CollectionPageInFront = ({ }, ]} > - + { const color = getColor(frontData.appearance) - const appearance = getAppearancePillar(frontData.appearance) + const pillar = getAppearancePillar(frontData.appearance) const [scrollX] = useState(() => new Animated.Value(0)) const flatListRef = useRef() @@ -163,13 +161,14 @@ const FrontWithResponse = ({ }) => ( diff --git a/projects/Mallard/src/components/front/layouts.ts b/projects/Mallard/src/components/front/layouts.ts index b10dddc38c..8b58442eb9 100644 --- a/projects/Mallard/src/components/front/layouts.ts +++ b/projects/Mallard/src/components/front/layouts.ts @@ -6,6 +6,7 @@ import { SplitImageItem, SmallItem, } from './item/item' +import { FrontCardAppearance } from 'src/common' const splashPage: PageLayout = withSlots([ { @@ -100,12 +101,14 @@ const sixStoryPage: PageLayout = withSlots([ }, ]) -export { - splashPage, - superHeroPage, - twoStoryPage, - threeStoryPage, - fourStoryPage, - fiveStoryPage, - sixStoryPage, +const layouts: { [key in FrontCardAppearance]: PageLayout } = { + [FrontCardAppearance.splashPage]: splashPage, + [FrontCardAppearance.superHeroPage]: superHeroPage, + [FrontCardAppearance.twoStoryPage]: twoStoryPage, + [FrontCardAppearance.threeStoryPage]: threeStoryPage, + [FrontCardAppearance.fourStoryPage]: fourStoryPage, + [FrontCardAppearance.fiveStoryPage]: fiveStoryPage, + [FrontCardAppearance.sixStoryPage]: sixStoryPage, } + +export { layouts } diff --git a/projects/Mallard/src/helpers/transform.ts b/projects/Mallard/src/helpers/transform.ts index 88091239eb..dfd98e3648 100644 --- a/projects/Mallard/src/helpers/transform.ts +++ b/projects/Mallard/src/helpers/transform.ts @@ -1,8 +1,14 @@ -import { Collection, CAPIArticle, Appearance } from 'src/common' +import { + Collection, + CAPIArticle, + Appearance, + FrontCardAppearance, +} from 'src/common' import { palette } from '@guardian/pasteup/palette' export interface FlatCard { collection: Collection + appearance: FrontCardAppearance | null articles: CAPIArticle[] } @@ -34,8 +40,9 @@ export const flattenCollectionsToCards = ( ): FlatCard[] => collections .map(collection => - collection.cards.map(({ articles }) => ({ + collection.cards.map(({ articles, appearance }) => ({ articles: Object.values(articles || {}), + appearance, collection, })), ) diff --git a/projects/archiver/main.ts b/projects/archiver/main.ts index 9112354096..678e67ceda 100644 --- a/projects/archiver/main.ts +++ b/projects/archiver/main.ts @@ -119,6 +119,8 @@ export const handler: Handler< { id?: string; index?: boolean }, void > = async event => { + console.log('Archiver lambda called with:') + console.log(JSON.stringify(event)) if (event.index) return summary() const id = event.id if (!(id && typeof id === 'string')) throw new Error('Nope') diff --git a/projects/aws/lib/aws-stack.ts b/projects/aws/lib/aws-stack.ts index 6d36a00b5f..70a90a28e3 100644 --- a/projects/aws/lib/aws-stack.ts +++ b/projects/aws/lib/aws-stack.ts @@ -51,6 +51,24 @@ export class EditionsStack extends cdk.Stack { }, ) + const cmsFrontsAccountIdParameter = new cdk.CfnParameter( + this, + 'cmsFronts-account-id-param', + { + type: 'String', + description: 'Account id for cmsFronts account.', + }, + ) + + const publishedEditionsBucketnameParameter = new cdk.CfnParameter( + this, + 'published-editions-bucket-name-param', + { + type: 'String', + description: 'Name for published editions bucket', + }, + ) + const deploy = s3.Bucket.fromBucketName( this, 'editions-dist', @@ -161,6 +179,10 @@ export class EditionsStack extends cdk.Stack { backend: `${gatewayId}.execute-api.eu-west-1.amazonaws.com/prod/`, //Yes, this (the region) really should not be hard coded. }, }) + new CfnOutput(this, 'archiver-arn', { + description: 'ARN for achiver lambda', + value: archiver.functionArn, + }) const archiverPolicy = new iam.PolicyStatement({ actions: ['*'], @@ -168,5 +190,23 @@ export class EditionsStack extends cdk.Stack { }) archiver.addToRolePolicy(archiverPolicy) + + const publishedBucket = s3.Bucket.fromBucketName( + this, + 'published-bucket', + publishedEditionsBucketnameParameter.valueAsString, + ) + + new lambda.CfnPermission( + this, + 'PublishedEditionsArchiverInvokePermission', + { + principal: 's3.amazonaws.com', + functionName: archiver.functionName, + action: 'lambda:InvokeFunction', + sourceAccount: cmsFrontsAccountIdParameter.valueAsString, + sourceArn: publishedBucket.bucketArn, + }, + ) } } diff --git a/projects/backend/utils/collection.ts b/projects/backend/utils/collection.ts index 1426197613..d379fd9aba 100644 --- a/projects/backend/utils/collection.ts +++ b/projects/backend/utils/collection.ts @@ -1,9 +1,10 @@ -import { CAPIArticle, Card } from '../common' import { - CollectionCardLayouts, - CollectionCardLayout, - cardLayouts, -} from '../../common/src/index' + CAPIArticle, + Card, + FrontCardsForArticleCount, + getCardAppearanceInfo, + getCardsForFront, +} from '../common' import { fromPairs } from 'ramda' import { PublishedFront } from '../fronts/issue' @@ -15,20 +16,27 @@ const chunk = (arr: T[], size: number) => const maxCardSize = 6 const getCardLayoutForArticles = ( - layout: CollectionCardLayouts, + layout: FrontCardsForArticleCount, articleLength: number, -): CollectionCardLayout => { - return layout[articleLength] || Object.values(layout).pop() +): FrontCardsForArticleCount[0] => { + if ( + articleLength === 1 || + articleLength === 2 || + articleLength === 3 || + articleLength === 4 || + articleLength === 5 || + articleLength === 6 + ) { + return layout[articleLength] + } + return Object.values(layout).pop() } export const createCardsFromAllArticlesInCollection = ( articles: [string, CAPIArticle][], front: PublishedFront, ): Card[] => { - const cardLayout = cardLayouts[front.name] - ? cardLayouts[front.name] - : cardLayouts.default - + const cardLayout = getCardsForFront(front.name) const layout = getCardLayoutForArticles(cardLayout, articles.length) /* @@ -38,18 +46,21 @@ export const createCardsFromAllArticlesInCollection = ( itemsSoFar: number cards: Card[] }>( - ({ itemsSoFar, cards }, current) => ({ - itemsSoFar: itemsSoFar + current, - cards: [ - ...cards, - { - layout: null, - articles: fromPairs( - articles.slice(itemsSoFar, itemsSoFar + current), - ), - }, - ], - }), + ({ itemsSoFar, cards }, current) => { + const { appearance, fits } = getCardAppearanceInfo(current) + return { + itemsSoFar: itemsSoFar + fits, + cards: [ + ...cards, + { + appearance, + articles: fromPairs( + articles.slice(itemsSoFar, itemsSoFar + fits), + ), + }, + ], + } + }, { itemsSoFar: 0, cards: [] }, ) @@ -63,7 +74,7 @@ export const createCardsFromAllArticlesInCollection = ( ...chunk(articles.slice(itemsSoFar), maxCardSize).map( groupOfArticles => { return { - layout: null, + appearance: null, articles: fromPairs(groupOfArticles), } }, diff --git a/projects/backend/yarn.lock b/projects/backend/yarn.lock index f3565f4204..3d22c06b10 100644 --- a/projects/backend/yarn.lock +++ b/projects/backend/yarn.lock @@ -106,6 +106,13 @@ core-js "^2.6.5" regenerator-runtime "^0.13.2" +"@babel/runtime@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" + integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" diff --git a/projects/common/package.json b/projects/common/package.json new file mode 100644 index 0000000000..256c0bdae3 --- /dev/null +++ b/projects/common/package.json @@ -0,0 +1,7 @@ +{ + "name": "@guardian/editions-common", + "@notes": "metro bundler needs babel here to not crap out :(.", + "dependencies": { + "@babel/runtime": "^7.5.5" + } +} diff --git a/projects/common/src/CollectionCardLayouts.ts b/projects/common/src/CollectionCardLayouts.ts deleted file mode 100644 index 158c3ef8f2..0000000000 --- a/projects/common/src/CollectionCardLayouts.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { CollectionCardLayout } from './index' -export type CollectionCardLayouts = { - [countOfArticles: number]: CollectionCardLayout -} diff --git a/projects/common/src/collection/card-layouts.ts b/projects/common/src/collection/card-layouts.ts index 684fc68b4f..39c4f5d874 100644 --- a/projects/common/src/collection/card-layouts.ts +++ b/projects/common/src/collection/card-layouts.ts @@ -1,36 +1,110 @@ -import { CollectionCardLayoutsForFront } from '../index' - -export const cardLayouts: CollectionCardLayoutsForFront = { - Crosswords: { - 0: [], - 1: [2], - }, - default: { - 0: [], - 1: [1], - 2: [1, 1], - 3: [1, 2], - 4: [1, 3], - 5: [1, 4], - 6: [1, 5], - 7: [1, 2, 4], - 8: [1, 3, 4], - 9: [1, 3, 5], - 10: [1, 4, 5], - 11: [1, 2, 3, 5], - 12: [1, 2, 4, 5], - 13: [1, 3, 4, 5], - 14: [1, 3, 4, 6], - 15: [1, 2, 3, 4, 5], - 16: [1, 2, 3, 4, 6], - 17: [1, 2, 3, 5, 6], - 18: [1, 2, 4, 5, 6], - 19: [1, 3, 4, 5, 6], - 20: [1, 3, 4, 1, 5, 6], - 21: [1, 2, 3, 4, 5, 6], - 22: [1, 3, 4, 3, 5, 6], - 23: [1, 3, 4, 5, 4, 6], - 24: [1, 3, 5, 4, 5, 6], - 25: [1, 5, 4, 5, 4, 6], - }, +type PossibleCardlengths = 1 | 2 | 3 | 4 | 5 | 6 + +export enum FrontCardAppearance { + splashPage = 'splash', + superHeroPage = 'super', + twoStoryPage = 'two', + threeStoryPage = 'three', + fourStoryPage = 'four', + fiveStoryPage = 'five', + sixStoryPage = 'six', +} + +export type FrontCardAppearanceShort = PossibleCardlengths | FrontCardAppearance + +export type FrontCardList = FrontCardAppearanceShort[] + +export interface FrontCardsForArticleCount { + [countOfArticles: number]: FrontCardList +} +export interface FrontCardAppearanceInfo { + fits: PossibleCardlengths +} + +const frontCardAppearanceInfo: { + [key in FrontCardAppearance]: FrontCardAppearanceInfo +} = { + [FrontCardAppearance.splashPage]: { fits: 1 }, + [FrontCardAppearance.superHeroPage]: { fits: 1 }, + [FrontCardAppearance.twoStoryPage]: { fits: 2 }, + [FrontCardAppearance.threeStoryPage]: { fits: 3 }, + [FrontCardAppearance.fourStoryPage]: { fits: 4 }, + [FrontCardAppearance.fiveStoryPage]: { fits: 5 }, + [FrontCardAppearance.sixStoryPage]: { fits: 6 }, +} + +export const defaultCardAppearances: { + [key in PossibleCardlengths]: FrontCardAppearance +} = { + 1: FrontCardAppearance.superHeroPage, + 2: FrontCardAppearance.twoStoryPage, + 3: FrontCardAppearance.threeStoryPage, + 4: FrontCardAppearance.fourStoryPage, + 5: FrontCardAppearance.fiveStoryPage, + 6: FrontCardAppearance.sixStoryPage, +} + +export const getCardAppearanceInfo = ( + card: FrontCardAppearanceShort, +): FrontCardAppearanceInfo & { appearance: FrontCardAppearance } => { + if (typeof card === 'number') { + return { + appearance: defaultCardAppearances[card], + ...frontCardAppearanceInfo[defaultCardAppearances[card]], + } + } + return { + appearance: card, + ...frontCardAppearanceInfo[card], + } +} + +const defaultLayout = ( + cover: FrontCardAppearanceShort = 1, +): FrontCardsForArticleCount => ({ + 0: [], + 1: [cover], + 2: [cover, 1], + 3: [cover, 2], + 4: [cover, 3], + 5: [cover, 4], + 6: [cover, 5], + 7: [cover, 2, 4], + 8: [cover, 3, 4], + 9: [cover, 3, 5], + 10: [cover, 4, 5], + 11: [cover, 2, 3, 5], + 12: [cover, 2, 4, 5], + 13: [cover, 3, 4, 5], + 14: [cover, 3, 4, 6], + 15: [cover, 2, 3, 4, 5], + 16: [cover, 2, 3, 4, 6], + 17: [cover, 2, 3, 5, 6], + 18: [cover, 2, 4, 5, 6], + 19: [cover, 3, 4, 5, 6], + 20: [cover, 3, 4, 1, 5, 6], + 21: [cover, 2, 3, 4, 5, 6], + 22: [cover, 3, 4, 3, 5, 6], + 23: [cover, 3, 4, 5, 4, 6], + 24: [cover, 3, 5, 4, 5, 6], + 25: [cover, 5, 4, 5, 4, 6], +}) + +export const getCardsForFront = ( + frontName: string, +): FrontCardsForArticleCount => { + switch (frontName) { + case 'Crosswords': + return { + 0: [], + 1: [2], + } + case 'Lifestyle': + case 'Food': + case 'Culture': + case 'Review': + return defaultLayout(FrontCardAppearance.splashPage) + default: + return defaultLayout(1) + } } diff --git a/projects/common/src/index.ts b/projects/common/src/index.ts index cbc1dbaae2..bcd6643a4a 100644 --- a/projects/common/src/index.ts +++ b/projects/common/src/index.ts @@ -1,4 +1,5 @@ -export { cardLayouts } from './collection/card-layouts' +import { FrontCardAppearance } from './collection/card-layouts' +export * from './collection/card-layouts' interface WithKey { key: string @@ -29,7 +30,7 @@ interface PillarAppearance { export type Appearance = PillarAppearance | ColorAppearance export interface Card { - layout: null + appearance: FrontCardAppearance | null articles: { [key: string]: CAPIArticle } } @@ -225,15 +226,6 @@ export interface Crossword { dateSolutionAvailable?: CapiDateTime } -export type CollectionCardLayout = number[] -export interface CollectionCardLayouts { - [countOfArticles: number]: CollectionCardLayout -} -export interface CollectionCardLayoutsForFront { - default: CollectionCardLayouts - [frontName: string]: CollectionCardLayouts -} - export const issueDir = (issueId: string) => `${issueId}` export const issuePath = (issueId: string) => `${issueDir(issueId)}/issue` diff --git a/projects/common/yarn.lock b/projects/common/yarn.lock new file mode 100644 index 0000000000..8f511de8be --- /dev/null +++ b/projects/common/yarn.lock @@ -0,0 +1,15 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" + integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== + dependencies: + regenerator-runtime "^0.13.2" + +regenerator-runtime@^0.13.2: + version "0.13.3" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" + integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==