Skip to content

Add support for CSS modules with SSR consistency #103606

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

Open
wants to merge 8 commits into
base: trunk
Choose a base branch
from
Open
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
5 changes: 3 additions & 2 deletions client/components/section/index.tsx
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ interface SectionProps {
subheader?: string | ReactElement;
children: ReactNode;
dark?: boolean;
className?: string;
}

interface SectionContainerProps {
@@ -76,10 +77,10 @@ export const SectionHeaderContainer = styled.div< SectionHeaderProps >`
const SectionContent = styled.div``;

const Section = ( props: SectionProps ) => {
const { children, header, subheader, dark } = props;
const { children, header, subheader, dark, className } = props;
/* eslint-disable wpcalypso/jsx-classname-namespace */
return (
<SectionContainer dark={ dark }>
<SectionContainer dark={ dark } className={ className }>
<SectionHeaderContainer>
<SectionHeader dark={ dark } className="wp-brand-font">
{ header }
19 changes: 19 additions & 0 deletions client/components/section/test/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @jest-environment jsdom
*/
import { render } from '@testing-library/react';
import Section from '../index';

describe( 'Section', () => {
it( 'applies additional className when provided', () => {
const customClassName = 'custom-section-class';
const { container } = render(
<Section header="Test Header" className={ customClassName }>
Test Content
</Section>
);

const sectionContainer = container.firstChild;
expect( sectionContainer ).toHaveClass( customClassName );
} );
} );
10 changes: 10 additions & 0 deletions client/declarations.d.ts
Original file line number Diff line number Diff line change
@@ -6,3 +6,13 @@ declare module 'is-my-json-valid' {
export default function ( schema: any, options?: any ): ( data: any ) => boolean;
export function filter( schema: any, options?: any ): any;
}

declare module '*.module.css' {
const content: { [ className: string ]: string };
export default content;
}

declare module '*.module.scss' {
const content: { [ className: string ]: string };
export default content;
}
79 changes: 1 addition & 78 deletions client/my-sites/plugins/education-footer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { Button } from '@automattic/components';
import { useOpenArticleInHelpCenter } from '@automattic/help-center/src/hooks';
import { useLocalizeUrl } from '@automattic/i18n-utils';
import styled from '@emotion/styled';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback } from 'react';
import FeatureItem from 'calypso/components/feature-item';
import LinkCard from 'calypso/components/link-card';
import Section, { SectionContainer } from 'calypso/components/section';
import { preventWidows } from 'calypso/lib/formatting';
import { addQueryArgs } from 'calypso/lib/route';
import PluginsResultsHeader from 'calypso/my-sites/plugins/plugins-results-header';
import { useDispatch, useSelector } from 'calypso/state';
import { recordTracksEvent } from 'calypso/state/analytics/actions/record';
import { isUserLoggedIn, getCurrentUserSiteCount } from 'calypso/state/current-user/selectors';
import { getSectionName } from 'calypso/state/ui/selectors';
import { isUserLoggedIn } from 'calypso/state/current-user/selectors';

const ThreeColumnContainer = styled.div`
@media ( max-width: 660px ) {
@@ -72,32 +66,6 @@ const EducationFooterContainer = styled.div`
}
`;

const MarketplaceContainer = styled.div< { isloggedIn: boolean } >`
--color-accent: var( --studio-blue-50 );
--color-accent-60: var( --studio-blue-60 );
margin-bottom: -32px;

.marketplace-cta {
min-width: 122px;
margin-bottom: 26px;

@media ( max-width: 660px ) {
margin-left: 16px;
margin-right: 16px;
}
}

${ ( { isloggedIn } ) =>
! isloggedIn &&
`${ SectionContainer } {
padding-bottom: 32px;
}` }

${ SectionContainer }::before {
background-color: #f6f7f7;
}
`;

const CardText = styled.span< { color: string } >`
color: ${ ( { color } ) => color };
font-family: 'SF Pro Text', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto',
@@ -107,51 +75,6 @@ const CardText = styled.span< { color: string } >`
line-height: 20px;
`;

export const MarketplaceFooter = () => {
const { __ } = useI18n();
const isLoggedIn = useSelector( isUserLoggedIn );
const currentUserSiteCount = useSelector( getCurrentUserSiteCount );
const sectionName = useSelector( getSectionName );

const startUrl = addQueryArgs(
{
ref: sectionName + '-lp',
},
sectionName === 'plugins' ? '/start/business' : '/start'
);

return (
<MarketplaceContainer isloggedIn={ isLoggedIn }>
<Section
header={ preventWidows( __( 'You pick the plugin. We’ll take care of the rest.' ) ) }
>
{ ( ! isLoggedIn || currentUserSiteCount === 0 ) && (
<Button className="is-primary marketplace-cta" href={ startUrl }>
{ __( 'Get Started' ) }
</Button>
) }
<ThreeColumnContainer>
Copy link
Member

Choose a reason for hiding this comment

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

I was almost about to say we should keep the ThreeColumnContainer in sync for the EducationFooter and MarketplaceFooter, but looking at the actual components they look pretty unrelated. Probably better that they're separated now.

<FeatureItem header={ __( 'Fully managed' ) }>
{ __(
'Premium plugins are fully managed by the team at WordPress.com. No security patches. No update nags. It just works.'
) }
</FeatureItem>
<FeatureItem header={ __( 'Thousands of plugins' ) }>
{ __(
'From WordPress.com premium plugins to thousands more community-authored plugins, we’ve got you covered.'
) }
</FeatureItem>
<FeatureItem header={ __( 'Flexible pricing' ) }>
{ __(
'Pay yearly and save. Or keep it flexible with monthly premium plugin pricing. It’s entirely up to you.'
) }
</FeatureItem>
</ThreeColumnContainer>
</Section>
</MarketplaceContainer>
);
};

const EducationFooter = () => {
const { __ } = useI18n();
const localizeUrl = useLocalizeUrl();
61 changes: 61 additions & 0 deletions client/my-sites/plugins/marketplace-footer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Button } from '@automattic/components';
import { useI18n } from '@wordpress/react-i18n';
import clsx from 'clsx';
import FeatureItem from 'calypso/components/feature-item';
import Section from 'calypso/components/section';
import { preventWidows } from 'calypso/lib/formatting';
import { addQueryArgs } from 'calypso/lib/route';
import { useSelector } from 'calypso/state';
import { isUserLoggedIn, getCurrentUserSiteCount } from 'calypso/state/current-user/selectors';
import { getSectionName } from 'calypso/state/ui/selectors';
import styles from './style.module.css';

export const MarketplaceFooter = () => {
const { __ } = useI18n();
const isLoggedIn = useSelector( isUserLoggedIn );
const currentUserSiteCount = useSelector( getCurrentUserSiteCount );
const sectionName = useSelector( getSectionName );

const startUrl = addQueryArgs(
{
ref: sectionName + '-lp',
},
sectionName === 'plugins' ? '/start/business' : '/start'
);

return (
<div className={ styles[ 'marketplace-footer' ] }>
<Section
header={ preventWidows( __( 'You pick the plugin. We’ll take care of the rest.' ) ) }
className={ clsx( styles[ 'marketplace-footer__section' ], {
[ styles[ 'is-logged-in' ] ]: isLoggedIn,
} ) }
>
{ ( ! isLoggedIn || currentUserSiteCount === 0 ) && (
<Button primary className={ styles[ 'marketplace-footer__cta' ] } href={ startUrl }>
{ __( 'Get Started' ) }
</Button>
) }
<div className={ styles[ 'marketplace-footer__three-column' ] }>
<FeatureItem header={ __( 'Fully managed' ) }>
{ __(
'Premium plugins are fully managed by the team at WordPress.com. No security patches. No update nags. It just works.'
) }
</FeatureItem>
<FeatureItem header={ __( 'Thousands of plugins' ) }>
{ __(
'From WordPress.com premium plugins to thousands more community-authored plugins, we’ve got you covered.'
) }
</FeatureItem>
<FeatureItem header={ __( 'Flexible pricing' ) }>
{ __(
'Pay yearly and save. Or keep it flexible with monthly premium plugin pricing. It’s entirely up to you.'
) }
</FeatureItem>
</div>
</Section>
</div>
);
};

export default MarketplaceFooter;
33 changes: 33 additions & 0 deletions client/my-sites/plugins/marketplace-footer/style.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.marketplace-footer {
--color-accent: var( --studio-blue-50 );
--color-accent-60: var( --studio-blue-60 );
margin-bottom: -32px;

& .marketplace-footer__section::before {
background-color: var( --studio-gray-0 );
}
}

.marketplace-footer__section.is-logged-in {
padding-bottom: 32px;
}

.marketplace-footer__cta {
min-width: 122px;
margin-bottom: 26px;

@media ( max-width: 660px ) {
margin-left: 16px;
margin-right: 16px;
}
}

.marketplace-footer__three-column {
display: flex;
flex-wrap: wrap;
justify-content: space-between;

@media ( max-width: 660px ) {
padding: 0 16px;
}
}
2 changes: 1 addition & 1 deletion client/my-sites/plugins/plans/index.tsx
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import PromoSection, { Props as PromoSectionProps } from 'calypso/components/pro
import { Gridicon } from 'calypso/devdocs/design/playground-scope';
import PageViewTracker from 'calypso/lib/analytics/page-view-tracker';
import PlansFeaturesMain from 'calypso/my-sites/plans-features-main';
import { MarketplaceFooter } from 'calypso/my-sites/plugins/education-footer';
import MarketplaceFooter from 'calypso/my-sites/plugins/marketplace-footer';
import { useDispatch, useSelector } from 'calypso/state';
import { appendBreadcrumb } from 'calypso/state/breadcrumb/actions';
import { getBreadcrumbs } from 'calypso/state/breadcrumb/selectors';
2 changes: 1 addition & 1 deletion client/my-sites/plugins/plugin-details.jsx
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ import {
isRequestingSites as checkRequestingSites,
} from 'calypso/state/sites/selectors';
import { getSelectedSite } from 'calypso/state/ui/selectors';
import { MarketplaceFooter } from './education-footer';
import MarketplaceFooter from './marketplace-footer';
import NoPermissionsError from './no-permissions-error';
import { usePluginIsMaintained } from './use-plugin-is-maintained';

2 changes: 1 addition & 1 deletion client/my-sites/plugins/plugins-browser/index.jsx
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import PageViewTracker from 'calypso/lib/analytics/page-view-tracker';
import useScrollAboveElement from 'calypso/lib/use-scroll-above-element';
import Categories from 'calypso/my-sites/plugins/categories';
import { useCategories } from 'calypso/my-sites/plugins/categories/use-categories';
import { MarketplaceFooter } from 'calypso/my-sites/plugins/education-footer';
import MarketplaceFooter from 'calypso/my-sites/plugins/marketplace-footer';
import NoPermissionsError from 'calypso/my-sites/plugins/no-permissions-error';
import useIsVisible from 'calypso/my-sites/plugins/plugins-browser/use-is-visible';
import SearchBoxHeader from 'calypso/my-sites/plugins/search-box-header';
7 changes: 6 additions & 1 deletion client/webpack.config.node.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

const path = require( 'path' );
const FileConfig = require( '@automattic/calypso-build/webpack/file-loader' );
const SassConfig = require( '@automattic/calypso-build/webpack/sass' );
const TranspileConfig = require( '@automattic/calypso-build/webpack/transpile' );
const { shouldTranspileDependency } = require( '@automattic/calypso-build/webpack/util' );
const webpack = require( 'webpack' );
@@ -127,8 +128,12 @@ const webpackConfig = {
include: shouldTranspileDependency,
} ),
fileLoader,
SassConfig.loader( {
includePaths: [ __dirname ],
extract: false,
} ),
{
test: /\.(sc|sa|c)ss$/,
test: /(?<!\.module)\.s?css$/,
loader: 'ignore-loader',
},
],
26 changes: 23 additions & 3 deletions packages/calypso-build/webpack/sass.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
const { execSync } = require( 'node:child_process' );
const { createHash } = require( 'node:crypto' );
const WebpackRTLPlugin = require( '@automattic/webpack-rtl-plugin' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
const MiniCSSWithRTLPlugin = require( './mini-css-with-rtl' );

/** @type {string | undefined} */
let gitHeadSha;
try {
gitHeadSha = execSync( 'git rev-parse HEAD' ).toString();
} catch {}

/**
* Return a webpack loader object containing our styling (Sass -> CSS) stack.
* @param {Object} _ Options
* @param {string[]} _.includePaths Sass files lookup paths
* @param {string} _.prelude String to prepend to each Sass file
* @param {Object} _.postCssOptions PostCSS options
* @param {boolean} _.extract Whether to extract the CSS into a separate file
* @returns {Object} webpack loader object
*/
module.exports.loader = ( { includePaths, prelude, postCssOptions } ) => ( {
module.exports.loader = ( { includePaths, prelude, postCssOptions, extract = true } ) => ( {
test: /\.(sc|sa|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
extract ? MiniCssExtractPlugin.loader : undefined,
{
loader: require.resolve( 'css-loader' ),
options: {
importLoaders: 2,
modules: {
exportOnlyLocals: ! extract,
auto: /\.module\.s?css$/,
getLocalIdent: ( _context, _localIdentName, localName ) =>
localName +
'_' +
createHash( 'md5' )
.update( localName + gitHeadSha )
.digest( 'hex' )
.slice( 0, 5 ),
},
// We do not want css-loader to resolve absolute paths. We
// typically use `/` to indicate the start of the base URL,
// but starting with css-loader v4, it started trying to handle
@@ -46,7 +66,7 @@ module.exports.loader = ( { includePaths, prelude, postCssOptions } ) => ( {
warnRuleAsWarning: true,
},
},
],
].filter( Boolean ),
} );

/**