diff --git a/.gitignore b/.gitignore index 90b39feed79..48ef45960a6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ node_modules /polaris-tokens/build /polaris.shopify.com/public/sitemap.xml /polaris.shopify.com/public/og-images +/polaris.shopify.com/public/icons +/polaris.shopify.com/public/playroom diff --git a/.prettierignore b/.prettierignore index bfe9335a78a..e41b0709127 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ dist node_modules /polaris-react/build /polaris-react/build-internal +/polaris.shopify.com/public/sandbox diff --git a/package.json b/package.json index 92a4c38086c..32ee9c96f64 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "preversion-packages": "turbo run preversion", "version-packages": "yarn preversion-packages && changeset version", "release": "turbo run build --filter=polaris.shopify.com^... && changeset publish", - "preversion": "echo \"Error: use @changsets/cli to version packages\" && exit 1" + "preversion": "echo \"Error: use @changsets/cli to version packages\" && exit 1", + "postinstall": "patch-package" }, "devDependencies": { "@babel/core": "^7.15.0", @@ -68,6 +69,8 @@ "jest-preset-stylelint": "^5.0.3", "jest-watch-typeahead": "^1.0.0", "npm-run-all": "^4.1.5", + "patch-package": "^6.4.7", + "postinstall-postinstall": "^2.1.0", "prettier": "^2.5.0", "rollup": "^2.70.2", "rollup-plugin-node-externals": "^4.0.0", diff --git a/patches/playroom+0.28.0.patch b/patches/playroom+0.28.0.patch new file mode 100644 index 00000000000..d69d94d5a0b --- /dev/null +++ b/patches/playroom+0.28.0.patch @@ -0,0 +1,80 @@ +diff --git a/node_modules/playroom/README.md b/node_modules/playroom/README.md +index 6c82bbe..f05b80b 100644 +--- a/node_modules/playroom/README.md ++++ b/node_modules/playroom/README.md +@@ -160,6 +160,12 @@ export { themeB } from './themeB'; + // etc... + ``` + ++## Additional Code Transformations ++ ++A hook into the internal processing of code is available via the `processCode` option, which is a path to a file that exports a function that receives the code as entered into the editor, and returns the new code to be rendered. ++ ++One example is [wrapping code in an IIFE for state support](https://github.com/seek-oss/playroom/issues/66). ++ + ## TypeScript Support + + If a `tsconfig.json` file is present in your project, static prop types are parsed using [react-docgen-typescript](https://github.com/styleguidist/react-docgen-typescript) to provide better autocompletion in the Playroom editor. +diff --git a/node_modules/playroom/lib/defaultModules/processCode.js b/node_modules/playroom/lib/defaultModules/processCode.js +new file mode 100644 +index 0000000..36a436c +--- /dev/null ++++ b/node_modules/playroom/lib/defaultModules/processCode.js +@@ -0,0 +1 @@ ++export default code => code; +diff --git a/node_modules/playroom/lib/makeWebpackConfig.js b/node_modules/playroom/lib/makeWebpackConfig.js +index 56defa7..1e7cf3b 100644 +--- a/node_modules/playroom/lib/makeWebpackConfig.js ++++ b/node_modules/playroom/lib/makeWebpackConfig.js +@@ -54,6 +54,9 @@ module.exports = async (playroomConfig, options) => { + __PLAYROOM_ALIAS__USE_SCOPE__: playroomConfig.scope + ? relativeResolve(playroomConfig.scope) + : require.resolve('./defaultModules/useScope'), ++ __PLAYROOM_ALIAS__PROCESS_CODE__: playroomConfig.processCode ++ ? relativeResolve(playroomConfig.processCode) ++ : require.resolve('./defaultModules/processCode'), + }, + }, + module: { +diff --git a/node_modules/playroom/src/utils/compileJsx.ts b/node_modules/playroom/src/utils/compileJsx.ts +index dadea77..82d080c 100644 +--- a/node_modules/playroom/src/utils/compileJsx.ts ++++ b/node_modules/playroom/src/utils/compileJsx.ts +@@ -1,9 +1,18 @@ + import { transform } from '@babel/standalone'; ++/* eslint-disable-next-line import/no-unresolved */ ++import processCode from '__PLAYROOM_ALIAS__PROCESS_CODE__'; + +-export const compileJsx = (code: string) => +- transform(`${code.trim() || ''}`, { ++export const compileJsx = (code: string) => { ++ const processedCode = processCode(code); ++ ++ if (typeof processedCode !== 'string') { ++ throw new Error('processCode function must return a string of code.'); ++ } ++ ++ return transform(`${processedCode.trim()}`, { + presets: ['react'], + }).code; ++} + + export const validateCode = (code: string) => { + try { +diff --git a/node_modules/playroom/src/utils/formatting.ts b/node_modules/playroom/src/utils/formatting.ts +index a1819bf..70ac15c 100644 +--- a/node_modules/playroom/src/utils/formatting.ts ++++ b/node_modules/playroom/src/utils/formatting.ts +@@ -133,10 +133,10 @@ export const formatAndInsert = ({ + snippet, + }); + +- return formatCode({ ++ return { + code: newCode, + cursor: updatedCursor, +- }); ++ }; + }; + + export const formatForInsertion = ({ diff --git a/polaris-react/src/index.ts b/polaris-react/src/index.ts index ff69562764e..ea35e6dc53e 100644 --- a/polaris-react/src/index.ts +++ b/polaris-react/src/index.ts @@ -413,3 +413,4 @@ export { SELECT_ALL_ITEMS as INDEX_TABLE_SELECT_ALL_ITEMS, SelectionType as IndexTableSelectionType, } from './utilities/index-provider'; +export {useBreakpoints} from './utilities/breakpoints'; diff --git a/polaris.shopify.com/.eslintrc.js b/polaris.shopify.com/.eslintrc.js index 4cc34b40893..417686365bf 100644 --- a/polaris.shopify.com/.eslintrc.js +++ b/polaris.shopify.com/.eslintrc.js @@ -1,5 +1,6 @@ module.exports = { root: true, extends: ['next/core-web-vitals'], + ignorePatterns: ['public/sandbox'], rules: {}, }; diff --git a/polaris.shopify.com/constants.js b/polaris.shopify.com/constants.js new file mode 100644 index 00000000000..f57d926882d --- /dev/null +++ b/polaris.shopify.com/constants.js @@ -0,0 +1,6 @@ +// Not a TS file because our playroom.config.js needs to access it also, and can't understand ts imports. +module.exports = { + playroom: { + baseUrl: '/playroom/', + }, +}; diff --git a/polaris.shopify.com/next.config.js b/polaris.shopify.com/next.config.js index 3674e42d41f..57f7a9ba9b2 100644 --- a/polaris.shopify.com/next.config.js +++ b/polaris.shopify.com/next.config.js @@ -8,6 +8,22 @@ const nextConfig = { experimental: { scrollRestoration: true, }, + async rewrites() { + return [ + // We want to rewrite the sandbox route in production + // to point at the public directory that our playroom assets are built to + // We leverage a rewrite here instead of a redirect in order to preserve + // a "pretty" url for the main playroom editor. + ...(process.env.NODE_ENV !== 'production' + ? [ + { + source: '/playroom/:path*', + destination: 'http://localhost:9000/:path*', + }, + ] + : []), + ]; + }, async headers() { return [ { @@ -31,6 +47,22 @@ const nextConfig = { async redirects() { return [ + // We run a redirect to port 9000 for non prod environments + // as playroom files aren't built to the public directory in dev mode. + // We redirect to /preview/index.html here because Playroom's webpack is configured + // to generate an html file for the preview page that reaches for assets in the root directory via a relative path. + // In this case we don't care about a pretty url, and want to make absolutely certain that the browser is pointing to preview/index.html + // such that it resolves the relative asset requests correctly. + { + source: '/playroom', + destination: '/playroom/index.html', + permanent: true, + }, + { + source: '/playroom/preview', + destination: '/playroom/preview/index.html', + permanent: true, + }, { source: '/components/get-started', destination: '/components', diff --git a/polaris.shopify.com/package.json b/polaris.shopify.com/package.json index 71dcbfb6ec6..5c37c185028 100644 --- a/polaris.shopify.com/package.json +++ b/polaris.shopify.com/package.json @@ -3,8 +3,8 @@ "version": "0.13.0", "private": true, "scripts": { - "build": "next build", - "dev": "open http://localhost:3000 && next dev", + "build": "yarn playroom:build && next build", + "dev": "concurrently \"open http://localhost:3000 && next dev\" \"npm:playroom:start\"", "start": "next start", "lint": "run-p lint:*", "lint:js": "TIMING=1 eslint --cache .", @@ -12,10 +12,12 @@ "clean": "rm -rf .turbo node_modules .next *.tsbuildinfo", "create-component": "generact --root src/components src/components/Template/Template.tsx", "gen-assets": "node scripts/gen-assets.mjs", - "get-props": "ts-node --skip-project ./scripts/get-props.ts" + "get-props": "ts-node --skip-project ./scripts/get-props.ts", + "playroom:start": "playroom start", + "playroom:build": "playroom build" }, "dependencies": { - "@floating-ui/react-dom-interactions": "^0.6.1", + "@floating-ui/react-dom-interactions": "^0.10.1", "@headlessui/react": "^1.6.5", "@shopify/polaris": "^10.0.0", "@shopify/polaris-icons": "^5.4.0", @@ -28,8 +30,8 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-markdown": "^8.0.2", - "use-dark-mode": "^2.3.1", - "remark-gfm": "^3.0.1" + "remark-gfm": "^3.0.1", + "use-dark-mode": "^2.3.1" }, "devDependencies": { "@types/gtag.js": "^0.0.10", @@ -38,16 +40,20 @@ "@types/node": "17.0.21", "@types/prismjs": "^1.26.0", "@types/react": "*", + "concurrently": "ˆ7.3.0", "eslint-config-next": "12.1.0", "eslint": "8.10.0", "execa": "^6.1.0", "frontmatter": "^0.0.3", + "babel-plugin-preval": "^5.1.0", "get-site-urls": "3.0.0-alpha.1", "generact": "^0.4.0", "globby": "^11.1.0", "js-yaml": "^4.1.0", + "playroom": "^0.28.0", "marked": "^4.0.16", "puppeteer": "^16.0.0", + "style-loader": "^3.3.1", "rehype-raw": "^6.1.1", "sass": "^1.49.9", "typescript": "^4.7.4" diff --git a/polaris.shopify.com/pages/_app.tsx b/polaris.shopify.com/pages/_app.tsx index bf3f2efed58..a737f8f92d7 100644 --- a/polaris.shopify.com/pages/_app.tsx +++ b/polaris.shopify.com/pages/_app.tsx @@ -1,7 +1,7 @@ import type {AppProps} from 'next/app'; import Head from 'next/head'; import Script from 'next/script'; -import {useEffect} from 'react'; +import {Fragment, useEffect} from 'react'; import {useRouter} from 'next/router'; import '../src/styles/globals.scss'; @@ -16,10 +16,17 @@ const gaPageView = (url: string) => { // Remove dark mode flicker. Minified version of https://github.com/donavon/use-dark-mode/blob/develop/noflash.js.txt const noflash = `!function(){var b="darkMode",g="dark-mode",j="light-mode";function d(a){document.body.classList.add(a?g:j),document.body.classList.remove(a?j:g)}var e="(prefers-color-scheme: dark)",c=window.matchMedia(e),h=c.media===e,a=null;try{a=localStorage.getItem(b)}catch(k){}var f=null!==a;if(f&&(a=JSON.parse(a)),f)d(a);else if(h)d(c.matches),localStorage.setItem(b,c.matches);else{var i=document.body.classList.contains(g);localStorage.setItem(b,JSON.stringify(i))}}()`; -function MyApp({Component, pageProps}: AppProps) { +function MyApp({Component, pageProps, ...appProps}: AppProps) { const router = useRouter(); const isProd = process.env.NODE_ENV === 'production'; + // We're using router.pathname here to check for a specific incoming route to render in a Fragment instead of + // the Page component. This will work fine for statically generated assets / pages + // Any SSR pages may break due to router sometimes being undefined on first render. + // see https://stackoverflow.com/questions/61040790/userouter-withrouter-receive-undefined-on-query-in-first-render + + const isLayoutNeeded = !appProps.router.asPath.startsWith('/sandbox'); + const LayoutComponent = isLayoutNeeded ? Page : Fragment; useEffect(() => { if (!isProd) return; @@ -64,7 +71,7 @@ function MyApp({Component, pageProps}: AppProps) { ) : null} - + @@ -74,7 +81,7 @@ function MyApp({Component, pageProps}: AppProps) { - + ); } diff --git a/polaris.shopify.com/pages/examples/data-table-with-fixed-first-columns.tsx b/polaris.shopify.com/pages/examples/data-table-with-fixed-first-columns.tsx index 6dd9f4a29d7..5587076bfa2 100644 --- a/polaris.shopify.com/pages/examples/data-table-with-fixed-first-columns.tsx +++ b/polaris.shopify.com/pages/examples/data-table-with-fixed-first-columns.tsx @@ -1,6 +1,5 @@ -import {Link, Page, Card, DataTable} from '@shopify/polaris'; +import {Link, Page, Card, DataTable, useBreakpoints} from '@shopify/polaris'; import {useState} from 'react'; -import {useMedia} from '../../src/utils/hooks'; import {withPolarisExample} from '../../src/components/PolarisExampleWrapper'; function DataTableWithFixedFirstColumnsExample() { @@ -277,8 +276,8 @@ function DataTableWithFixedFirstColumnsExample() { ], ]; const [sortedRows, setSortedRows] = useState(rows); - const showFixedColumns = useMedia('screen and (max-width: 850px)'); - const fixedFirstColumns = showFixedColumns ? 2 : 0; + const {mdDown, mdOnly} = useBreakpoints(); + const fixedFirstColumns = mdDown || mdOnly ? 2 : 0; return ( diff --git a/polaris.shopify.com/pages/sandbox.tsx b/polaris.shopify.com/pages/sandbox.tsx new file mode 100644 index 00000000000..49557d16ea6 --- /dev/null +++ b/polaris.shopify.com/pages/sandbox.tsx @@ -0,0 +1,86 @@ +import SandboxHeader from '../src/components/SandboxHeader'; +import useDarkMode from 'use-dark-mode'; +import {useEffect, useRef} from 'react'; +import {useRouter} from 'next/router'; +import type {InferGetServerSidePropsType, GetServerSideProps} from 'next'; + +export const getServerSideProps: GetServerSideProps = async ({query}) => { + // We need to pass initial query param to our nested iframe + const initialSearchParams = new URLSearchParams( + query as Record, + ).toString(); + return { + props: { + initialSearchParams: `?${initialSearchParams}`, + }, + }; +}; + +export default function Sandbox({ + initialSearchParams, +}: InferGetServerSidePropsType) { + const darkMode = useDarkMode(false); + const iframeRef = useRef(null); + const router = useRouter(); + const searchValue = useRef(''); + + useEffect(() => { + /** + * We want to mirror the iframes url in the parent (aka browser) to support URL sharing. + * the iframes onload handler isn't invoked when the iframes url changes so we're polling here instead. + */ + const iframeUrlPoll = setInterval(() => { + if ( + iframeRef?.current?.contentWindow && + iframeRef.current.contentWindow.location.search !== searchValue.current + ) { + searchValue.current = iframeRef.current.contentWindow.location.search; + const iframeQueryObj = Object.fromEntries( + new URLSearchParams(searchValue.current), + ); + + router.replace( + { + query: iframeQueryObj, + }, + undefined, + { + shallow: true, + }, + ); + } + }, 200); + return () => clearInterval(iframeUrlPoll); + }, []); + + return ( +
+ +