diff --git a/.changeset/fast-boats-matter.md b/.changeset/fast-boats-matter.md new file mode 100644 index 00000000000..44f90912ffe --- /dev/null +++ b/.changeset/fast-boats-matter.md @@ -0,0 +1,5 @@ +--- +'polaris.shopify.com': patch +--- + +Improved the design of the Sandbox feature. diff --git a/.changeset/gorgeous-rabbits-help.md b/.changeset/gorgeous-rabbits-help.md new file mode 100644 index 00000000000..bebbcf79fcb --- /dev/null +++ b/.changeset/gorgeous-rabbits-help.md @@ -0,0 +1,5 @@ +--- +'polaris.shopify.com': minor +--- + +Added Playroom integration to Polaris docs site. diff --git a/.gitignore b/.gitignore index 8242016ab75..38c1f9ed80c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ node_modules /polaris.shopify.com/.cache/ /polaris.shopify.com/public/sitemap.xml /polaris.shopify.com/public/og-images +/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 fe28bdece04..505153ecfc2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "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", - "new-migration": "yarn workspace @shopify/polaris-migrator generate" + "new-migration": "yarn workspace @shopify/polaris-migrator generate", + "postinstall": "patch-package" }, "devDependencies": { "@babel/core": "^7.15.0", @@ -73,6 +74,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.shopify.com/.eslintrc.js b/polaris.shopify.com/.eslintrc.js index 4cc34b40893..fd851b53a0b 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/playroom'], 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 d39c3a3084f..8a3349ee42e 100644 --- a/polaris.shopify.com/next.config.js +++ b/polaris.shopify.com/next.config.js @@ -8,9 +8,41 @@ 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 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 578bd6d6ae3..319c43049eb 100644 --- a/polaris.shopify.com/package.json +++ b/polaris.shopify.com/package.json @@ -3,10 +3,11 @@ "version": "0.21.1", "private": true, "scripts": { - "build": "yarn gen-assets && next build", + "build": "yarn gen-assets && playroom build && next build", "start": "next start", "dev": "run-p dev:*", "dev:server": "open http://localhost:3000 && next dev", + "dev:playroom": "playroom start", "dev:watch-md": "node scripts/watch-md.mjs", "lint": "run-p lint:*", "lint:js": "TIMING=1 eslint --cache .", @@ -18,7 +19,7 @@ "gen-assets": "node scripts/gen-assets.mjs" }, "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.7.0", "@shopify/polaris-icons": "^6.4.0", @@ -51,13 +52,16 @@ "eslint-config-next": "12.1.0", "execa": "^6.1.0", "frontmatter": "^0.0.3", + "babel-plugin-preval": "^5.1.0", "generact": "^0.4.0", "get-site-urls": "3.0.0-alpha.1", "globby": "^11.1.0", "js-yaml": "^4.1.0", "lodash.set": "^4.3.2", + "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 fbec1a2f9d7..481d191fbf2 100644 --- a/polaris.shopify.com/pages/_app.tsx +++ b/polaris.shopify.com/pages/_app.tsx @@ -23,6 +23,11 @@ function MyApp({Component, pageProps}: AppProps) { const isProd = process.env.NODE_ENV === 'production'; const darkMode = useDarkMode(false); + // 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 + useEffect(() => { if (!isProd) return; @@ -43,6 +48,7 @@ function MyApp({Component, pageProps}: AppProps) { }.png`; const isPolarisExample = router.asPath.startsWith('/examples'); + const isPolarisSandbox = router.asPath.startsWith('/sandbox'); useEffect(() => { document.documentElement.style.setProperty( @@ -91,7 +97,7 @@ function MyApp({Component, pageProps}: AppProps) { !isPolarisExample && 'styles-for-site-but-not-polaris-examples', )} > - {isPolarisExample ? ( + {isPolarisExample || isPolarisSandbox ? ( ) : ( diff --git a/polaris.shopify.com/pages/sandbox.tsx b/polaris.shopify.com/pages/sandbox.tsx new file mode 100644 index 00000000000..570a9544749 --- /dev/null +++ b/polaris.shopify.com/pages/sandbox.tsx @@ -0,0 +1,95 @@ +import {useEffect, useRef, useState} from 'react'; +import type {InferGetServerSidePropsType, GetServerSideProps} from 'next'; +import {useRouter} from 'next/router'; + +import SandboxHeader from '../src/components/SandboxHeader'; +import SandboxHelpDialog from '../src/components/SandboxHelpDialog'; +import SandboxContainer from '../src/components/SandboxContainer'; + +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}`, + }, + }; +}; + +const MS_DELAY_BEFORE_SHOW_ONBOARDING = 500; + +export default function Sandbox({ + initialSearchParams, +}: InferGetServerSidePropsType) { + const iframeRef = useRef(null); + const router = useRouter(); + const searchValue = useRef(''); + const [isHelpOpen, setHelpIsOpen] = useState(false); + + // After the page has rendered at least once, we might show the help dialog + // (so it animates onto the screen nicely) + useEffect(() => { + const helpTimeout = setTimeout(() => { + const hasAlreadyBeenOnboarded = localStorage.getItem('onboarded'); + if (hasAlreadyBeenOnboarded) { + return; + } + localStorage.setItem('onboarded', 'true'); + setHelpIsOpen(true); + }, MS_DELAY_BEFORE_SHOW_ONBOARDING); + return () => clearTimeout(helpTimeout); + }, []); + + 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); + }, [router]); + + return ( + + + +