Skip to content

Commit

Permalink
feat(e2e): use package updates + extensive setup with Mantine
Browse files Browse the repository at this point in the history
as requested in #34

good opportunity to test the lib with another great UI framework
and CSS-in-JS lib
  • Loading branch information
nibtime committed Jul 6, 2022
1 parent 83bd8c3 commit f935199
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 79 deletions.
37 changes: 34 additions & 3 deletions apps/e2e/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import {
ColorScheme,
ColorSchemeProvider,
MantineProvider,
} from "@mantine/core";
import { useHotkeys, useLocalStorage } from "@mantine/hooks";
import Script from "next/script";
import { useMemo } from "react";
// import '../styles/globals.css';
import globalStyles from "../styles/globalStyles";

Expand All @@ -7,8 +14,19 @@ const customInlineScriptWorker = `console.log('Hi I am inline-script running wit

const customInlineScriptAfter = `console.log('Hi I am an inline-script running with strategy afterInteractive')`;

function MyApp({ Component, pageProps }) {
globalStyles();
function MyApp({ Component, pageProps, ssrNonce }) {
const nonce = typeof window === 'undefined' ? ssrNonce : document.head.nonce

const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
key: "mantine-color-scheme",
defaultValue: "light",
getInitialValueInEffect: true,
});

const toggleColorScheme = (value?: ColorScheme) =>
setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"));

useHotkeys([["mod+J", () => toggleColorScheme()]]);
return (
<>
<Script
Expand Down Expand Up @@ -50,7 +68,20 @@ function MyApp({ Component, pageProps }) {
>
{customInlineScriptAfter}
</Script>
<Component {...pageProps} />
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<MantineProvider
theme={{ colorScheme }}
emotionOptions={{
key: "mantine",
nonce,
}}
>
<Component {...pageProps} />
</MantineProvider>
</ColorSchemeProvider>
</>
);
}
Expand Down
69 changes: 48 additions & 21 deletions apps/e2e/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Document, {
import {
getCspInitialProps,
provideComponents,
} from "@next-safe/middleware/dist/document";
import { Html, Main } from "next/document";
import Document, { Html, Main } from "next/document";
import React from "react";
import { getCssText } from "stitches.config";
import { createStylesServer, ServerStyles } from "@mantine/next";
import { lazyGetCssText } from "stitches.config";

const InterVar = `@font-face {
font-family: 'Inter var';
Expand All @@ -22,34 +24,59 @@ const InterVar = `@font-face {
font-named-instance: 'Italic';
}`;

// Output of Next.js font optimization for Rubik from Google Fonts
// the generated inline style isn't captured by this lib.
// Can be copied from output and added to CSP manually
// by passing it to `hashRawCss` of `getCspInitialProps`
// URLs seem to be stable: https://stackoverflow.com/questions/47638772/do-google-gstatic-font-urls-change
const InlinedGoogleFontRubik = `@font-face{font-family:'Rubik';font-style:italic;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWbBXyIfDnIV7nEt3KSJbVDV49rz8sDE0Uz.woff) format('woff')}@font-face{font-family:'Rubik';font-style:italic;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWbBXyIfDnIV7nEt3KSJbVDV49rz8tdE0Uz.woff) format('woff')}@font-face{font-family:'Rubik';font-style:italic;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWbBXyIfDnIV7nEt3KSJbVDV49rz8tvE0Uz.woff) format('woff')}@font-face{font-family:'Rubik';font-style:italic;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWbBXyIfDnIV7nEt3KSJbVDV49rz8uDFEUz.woff) format('woff')}@font-face{font-family:'Rubik';font-style:italic;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWbBXyIfDnIV7nEt3KSJbVDV49rz8u6FEUz.woff) format('woff')}@font-face{font-family:'Rubik';font-style:italic;font-weight:800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWbBXyIfDnIV7nEt3KSJbVDV49rz8vdFEUz.woff) format('woff')}@font-face{font-family:'Rubik';font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-WYi1Uw.woff) format('woff')}@font-face{font-family:'Rubik';font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-B4i1Uw.woff) format('woff')}@font-face{font-family:'Rubik';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-NYi1Uw.woff) format('woff')}@font-face{font-family:'Rubik';font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-2Y-1Uw.woff) format('woff')}@font-face{font-family:'Rubik';font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-4I-1Uw.woff) format('woff')}@font-face{font-family:'Rubik';font-style:normal;font-weight:800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-h4-1Uw.woff) format('woff')}@font-face{font-family:'Rubik';font-style:italic;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWEBXyIfDnIV7nEnXO61E_c5IhGzg.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Rubik';font-style:italic;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWEBXyIfDnIV7nEnXq61E_c5IhGzg.woff2) format('woff2');unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Rubik';font-style:italic;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWEBXyIfDnIV7nEnXy61E_c5IhGzg.woff2) format('woff2');unicode-range:U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F}@font-face{font-family:'Rubik';font-style:italic;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWEBXyIfDnIV7nEnXC61E_c5IhGzg.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Rubik';font-style:italic;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWEBXyIfDnIV7nEnX661E_c5Ig.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:'Rubik';font-style:normal;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWKBXyIfDnIV7nMrXyw023e1Ik.woff2) format('woff2');unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:'Rubik';font-style:normal;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWKBXyIfDnIV7nFrXyw023e1Ik.woff2) format('woff2');unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:'Rubik';font-style:normal;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWKBXyIfDnIV7nDrXyw023e1Ik.woff2) format('woff2');unicode-range:U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F}@font-face{font-family:'Rubik';font-style:normal;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWKBXyIfDnIV7nPrXyw023e1Ik.woff2) format('woff2');unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:'Rubik';font-style:normal;font-weight:300 800;font-display:swap;src:url(https://fonts.gstatic.com/s/rubik/v20/iJWKBXyIfDnIV7nBrXyw023e.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}`;

const stylesServer = createStylesServer();

export default class MyDocument extends Document {
static async getInitialProps(ctx) {
try {
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{/* Stitches CSS for SSR */}
<style
id="stitches"
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
</>
),
};
} finally {
}
const initialProps = await getCspInitialProps({
ctx,
trustifyStyles: true,
enhanceAppWithNonce: true,
hashRawCss: [
InlinedGoogleFontRubik,
(initialProps) => [
stylesServer.extractCritical(initialProps.html).css,
...stylesServer
.extractCriticalToChunks(initialProps.html)
.styles.map((s) => s.css),
],
],
});
initialProps.styles = (
<>
{initialProps.styles}
{/* Mantine CSS-in-JS SSR (Emotion) */}
<ServerStyles html={initialProps.html} server={stylesServer} />
</>
);
return initialProps;
}
render() {
// those components are automagically wired with provideHashesOrNonce
// those components get automagically wired with strictDynamic/strictInlineStyles middleware
const { Head, NextScript } = provideComponents(this.props);
return (
<Html>
<Head>
<script>{`console.log('Hello from _document/Head, I get nonced/hashed there')`}</script>
<style dangerouslySetInnerHTML={{ __html: InterVar }} />
{/* Stitches CSS-in-JS SSR */}
<style
id="stitches"
dangerouslySetInnerHTML={{
__html: lazyGetCssText(this.props.__NEXT_DATA__.page),
}}
/>
<link
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
Expand Down
46 changes: 15 additions & 31 deletions apps/e2e/pages/_middleware.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,33 @@
import {
chain,
csp,
nextSafe,
strictDynamic,
strictInlineStyles,
reporting,
} from "@next-safe/middleware";

const isDev = process.env.NODE_ENV === "development";
const reportOnly = !!process.env.CSP_REPORT_ONLY;

const nextSafeMiddleware = nextSafe((req) => {
return {
isDev,
contentSecurityPolicy: {
reportOnly,
"frame-src": [`'self'`, req.nextUrl.origin],
const securityMiddleware = [
nextSafe({ disableCsp: true }),
csp({
directives: {
"frame-src": ["self"],
"img-src": ["self", "data:", "https://images.unsplash.com"],
"font-src": ["self", "https://fonts.gstatic.com"],
"connect-src": ["self", "sentry.io"],
},
// customize as you need: https://trezy.gitbook.io/next-safe/usage/configuration
};
});

const reportingMiddleware = reporting((req) => {
const nextApiReportEndpoint = `/api/reporting`;
return {
}),
strictDynamic(),
strictInlineStyles(),
reporting({
csp: {
reportUri: process.env.CSP_REPORT_URI || nextApiReportEndpoint,
reportUri: "/api/reporting"
},
reportTo: {
max_age: 1800,
endpoints: [
{
url: process.env.REPORT_TO_ENDPOINT_DEFAULT || nextApiReportEndpoint,
},
],
endpoints: [{ url: "/api/reporting" }],
},
};
});

const securityMiddleware = [
nextSafeMiddleware,
strictDynamic(),
strictInlineStyles({
extendStyleSrc: false,
}),
reportingMiddleware,
];

export default chain(...securityMiddleware);
17 changes: 15 additions & 2 deletions apps/e2e/pages/api/reporting.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
import { reporting } from "@next-safe/middleware/dist/api";
import {
reporting,
sentryCspReporterForEndpoint,
} from "@next-safe/middleware/dist/api";

export default reporting(data => console.log(JSON.stringify(data)));
const sentryCspEndpoint = process.env.SENTRY_CSP_ENDPOINT;

const consoleLogReporter = (data) =>
console.log(JSON.stringify(data, undefined, 2));

export default reporting(
consoleLogReporter,
...(sentryCspEndpoint
? [sentryCspReporterForEndpoint(sentryCspEndpoint)]
: [])
);
4 changes: 2 additions & 2 deletions apps/e2e/pages/static-page.tsx → apps/e2e/pages/gsp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ const Page = ({ random }) => {
<h2>Internal navigation to other pages</h2>
<ul>
<li>
<Link href="/isr/lazy-slug">
<Link href="/isr/gsp">
<a>
Page with getStaticProps + <code>revalidate</code> (ISR)
</a>
</Link>
</li>
<li>
<Link href="/dynamic-page">Page with getServerSideProps</Link>
<Link href="/gssp">Page with getServerSideProps</Link>
</li>
</ul>
</Prose>
Expand Down
4 changes: 2 additions & 2 deletions apps/e2e/pages/dynamic-page.tsx → apps/e2e/pages/gssp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ const Page = ({ requestHeaders, responseHeaders }) => {
<h2>Internal navigation to other pages</h2>
<ul>
<li>
<Link href="/static-page">Page with getStaticProps</Link>
<Link href="/gsp">Page with getStaticProps</Link>
</li>
<li>
<Link href="/isr/i-am-a-lazy-path">
<Link href="/isr/gsp">
<a>
Page with getStaticProps + <code>revalidate</code> (ISR)
</a>
Expand Down
31 changes: 24 additions & 7 deletions apps/e2e/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,55 @@
import Prose from "components/Prose";
import Container from "components/Container";
import Hydrated from "components/Hydrated";
import Link from "next/link";

// pages without a data fetching function are static pages and must use a Hash-based CSP.
const Page = () => {
return (
<Container isCentered>
<Prose>
<h1>@next-safe/middleware e2e test app</h1>
<h1>@next-safe/middleware e2e app</h1>
<Hydrated />
<p>
An app to e2e test the strict CSP capabilities of the
A Next.js app to test the strict CSP capabilities of the{" "}
<a href="https://www.npmjs.com/package/@next-safe/middleware">
@next-safe/middleware
</a>{" "}
package.
</p>
<h2>Prerendering strategies:</h2>
<p>This page has no data fetching method</p>
<ul>
<li>
<a href="/static-page">Page with getStaticProps</a> (Hash-based)
<a href="/gsp">Page with getStaticProps</a> (Hash-based)
</li>
<li>
<a href="/dynamic-page">Page with getServerSideProps</a>{" "}
(Nonce-based)
<a href="/gssp">Page with getServerSideProps</a> (Nonce-based)
</li>
<li>
<a href="/isr/lazy-slug">
<a href="/isr/gsp">
Page with getStaticProps + <code>revalidate</code> (ISR)
</a>{" "}
(Hash-based)
</li>
</ul>
<h2>With Mantine:</h2>
<ul>
<li>
<a href="/mantine">No data fetching method</a> (Hash-based)
</li>
<li>
<a href="/mantine/gsp">
With <code>getStaticProps</code>
</a>{" "}
(Hash-based)
</li>
<li>
<a href="/mantine/gssp">
With <code>getServerSideProps</code>
</a>{" "}
(Nonce-based)
</li>
</ul>
</Prose>
</Container>
);
Expand Down
6 changes: 3 additions & 3 deletions apps/e2e/pages/isr/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const config = {

export const getStaticPaths = async () => {
// as long as we build-time prerender at least one path, it will work with Hash-based strict CSP.
const path = "static-incremental";
const path = "gsp";
return {
paths: [{ params: { slug: path } }],
fallback: "blocking",
Expand Down Expand Up @@ -71,10 +71,10 @@ const Page = ({ random, revalidate }) => {
<h2>Internal navigation to other pages:</h2>
<ul>
<li>
<Link href="/static-page">Page with getStaticProps</Link>
<Link href="/gsp">Page with getStaticProps</Link>
</li>
<li>
<Link href="/dynamic-page">Page with getServerSideProps</Link>
<Link href="/gssp">Page with getServerSideProps</Link>
</li>
</ul>
</Prose>
Expand Down
7 changes: 7 additions & 0 deletions apps/e2e/pages/mantine/gsp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import MantinePage from "./index";

export const getStaticProps = () => {
return { props: {} };
};

export default MantinePage;
7 changes: 7 additions & 0 deletions apps/e2e/pages/mantine/gssp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import MantinePage from "./index";

export const getServerSideProps = () => {
return { props: {} };
};

export default MantinePage;
Loading

0 comments on commit f935199

Please sign in to comment.