Skip to content

Commit

Permalink
playground code
Browse files Browse the repository at this point in the history
  • Loading branch information
lukesmurray committed Mar 24, 2022
1 parent 8753d46 commit f200a98
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 0 deletions.
220 changes: 220 additions & 0 deletions components/Live.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/* eslint-disable unused-imports/no-unused-imports */
import * as Babel from "@babel/standalone";
import babelPluginEmotion from "@emotion/babel-plugin";
import babelPluginMacros from "babel-plugin-macros";
import theme from "prism-react-renderer/themes/vsDark";
import { useContext } from "react";
import {
LiveContext,
LiveEditor,
LiveError,
LivePreview,
LiveProvider,
} from "react-live";
import invariant from "tiny-invariant";
import tw, { css } from "twin.macro";

import { ProseWrapper } from "./defaultMDXComponents";

// we can include macros at runtime without them being stripped
// away by babel-plugin-macros during compilation as long as they are
// imported dynamically.
// in order to get dynamic imports working we have to use a top level await
// which we have to enable in next.config and tsconfig.json
const twinImport = await import("twin.macro");

// if you use `twin.macro`s `css` import then the plugin will add
// import { css as _css } from "@emotion/react";
// to your code.
// we don't want imports so we strip it out and add the import here
// and pass the `_css` value to the plugin as scope
// however when I add the css import to the top of this file
// import { css } from "@emotion/react";
// then the `css` prop becomes undefined in this file
// dynamically importing the package avoid the issue
const emotionImport = await import("@emotion/react");
const emotionRuntimeImport = await import("@emotion/react/jsx-runtime");
const emotionStyledImport = await import("@emotion/styled/base");

export function Live({
code,
scope,
centerExample,
noInline = false,
}: {
code: string;
scope?: { [key: string]: any };
centerExample?: boolean;
noInline?: boolean;
}) {
return (
<LiveProvider
code={code}
// live provider defaults to inline=True but our transpiled code is never inline
// inline methods are basically anything that can be wrapped in a return statement.
// e.g. return (<div>Hello World</div>)
// however regardless of what the user provides, we always add tailwind imports
// to the top of the user's code.
// the resulting transpiled code is never inline so we set noInline to true
noInline={true}
theme={theme}
// this is the scope that is passed to the transpiled code
// to get the correct names of the variables I just transpiled all the
// examples from the twin.macro readme and looked at the output
scope={{
...scope,
//@ts-expect-error jsx is not defined in the emotion types
_jsx: emotionRuntimeImport.jsx,
//@ts-expect-error jsxs is not defined in the emotion types
_jsxs: emotionRuntimeImport.jsxs,
_css: emotionImport.css,
_styled2: emotionStyledImport.default,
process,
}}
transformCode={async (code) => {
const DEBUG = true;

// see comment above on the noInline prop.
// if noInline is false (meaining inline=True) then we need to surround
// the user's code with a render call
// see https://github.com/FormidableLabs/react-live/blob/98925cc473f3b3146dd11e3281a644e1ea8a18f5/src/utils/transpile/index.js#L14-L35https://github.com/FormidableLabs/react-live/blob/98925cc473f3b3146dd11e3281a644e1ea8a18f5/src/utils/transpile/index.js#L14-L35
// to understand why
if (!noInline) {
code = `render(${code})`;
}

// we manually add twin.macro imports to the top of the user's code
// so that they are transpiled and removed by the twin macro
code = `import tw, { css, styled, screen, theme } from "twin.macro";\n${code}`;

// helpful for seeing code passed to babel
if (DEBUG) {
console.log("******************* pre transform *******************");
console.log(code);
}

// now we transpile the code
code =
Babel.transform(code, {
// see emotion css prop documentation https://emotion.sh/docs/css-prop
// the react preset settings and emotion babel-plugin settings are
// copied from there based on the new-jsx runtime configuration
presets: [
[
"react",
{ runtime: "automatic", importSource: "@emotion/react" },
],
],
plugins: [
babelPluginEmotion,
[
// add babel plugin macros
babelPluginMacros,
{
/**
* Internally babelPluginMacros tries to resolve macros from
* the filesystem. That would throw errors in the browser so we
* provide our own resolve function that just returns the
* name of the macro
* @param source the id of the module to resolve (e.g. "twin.macro")
* @param basedir the directory to resolve the module from (e.g. "./src")
* @returns the path to the resolved module
*/
resolvePath: (source: string) => {
invariant("twin.macro" === source, "Unexpected macro");
return source;
},
/**
* babelPluginMacros tries to require macros from the filesystem.
* Instead we provide our own require function that just returns
* the asynchronously imported macro at runtime
* @param path
* @returns
*/
require: (path: string) => {
invariant("twin.macro" === path, "Unexpected macro");
return twinImport;
},
},
],
],
}).code ?? "";

// the transpiled code is run inside a function
// https://github.com/FormidableLabs/react-live/blob/98925cc473f3b3146dd11e3281a644e1ea8a18f5/src/utils/transpile/evalCode.js
// and I didn't find any obvious way to enable import statements inside the function.
// Normally if we need to import something we would add it to the scope
// but in this case the imports are generated by the babel plugins (emotion and twin.macro)
// to avoid runtime errors I just strip out all the imports and manually add them to scope
// to determine what these imports look like I just ran all the examples from the twin.macro readme
code = code.replace(
`import { jsx as _jsx } from "@emotion/react/jsx-runtime";`,
""
);
code = code.replace(
`import { css as _css } from "@emotion/react";`,
""
);
code = code.replace(
`import { jsxs as _jsxs } from "@emotion/react/jsx-runtime";`,
""
);
code = code.replace(`import _styled2 from "@emotion/styled/base";`, "");

if (DEBUG) {
console.log("------------------- post transform -------------------");
console.log(code);
}

// log the code object if you are struggling to debug react live errors
// console.log(code);
return code;
}}>
<LiveInner centerExample={centerExample} />
</LiveProvider>
);
}

function LiveInner({ centerExample = false }: { centerExample?: boolean }) {
const { error } = useContext(LiveContext);
return (
<div
css={[
tw`grid grid-flow-col rounded-md shadow-lg border overflow-hidden`,
css`
min-height: 9em;
grid-template-columns: minmax(5ch, min(50%, 45ch)) 1fr;
`,
]}>
<ProseWrapper>
<LiveEditor
spellCheck="false"
css={[
css`
& {
${tw`h-full`}
}
&,
& > pre {
${tw`min-h-full`}
border-radius: 0;
}
`,
]}
/>
</ProseWrapper>
{error && <LiveError css={tw`bg-red-400`} />}
{!error && (
<LivePreview
css={[
centerExample && tw`grid place-items-center`,
css`
padding: 10px;
`,
]}
/>
)}
</div>
);
}
63 changes: 63 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
webpack: (config, { isServer, webpack }) => {
if (!isServer) {
/**
* babel plugin macros tries to import cosmiconfig but cosmiconfig does not
* work in the browser. https://github.com/kentcdodds/babel-plugin-macros/issues/186#issuecomment-1077720900
* This might not be an issue but in nextjs it is a build error and you can't render
* your site until the build error is fixed
*/
config.plugins.push(
new webpack.IgnorePlugin({
/**
* check whether a resource and context should be ignored
* @param {string} resource the string that is being imported
* @param {string} context the absolute path of the file that is importing the resource
* @returns {boolean} true if the resource should be ignored
*/
checkResource(resource) {
// ensure cosmiconfig is not included in the client bundle
return resource.match(/^cosmiconfig$/);
},
})
);

// add NODE_ENV to the client bundle. It is used in the code generated by
// twin.macro during babel transpilation
config.plugins.push(
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
})
);

// emotion-babel-plugin and twin.macro both import fs
// which is not included in the browser
// to avoid a module not found error we set resolve.fallback to false
config.resolve.fallback = {
...(config.resolve.fallback || {}),
fs: false,
};
}

/**
* babel-plugin-macros strips macro imports from the bundle.
* Because we're using babel-plugin-macros ourselves, we have to dynamically import
* twin at runtime to "hide" the import from babel-plugin-macros
* so that it is available in the live coding environment.
*
* also if we manually import css from "emotion/react" it breaks the css prop.
* in order to have css available in the live coding environment we have to
* dynamically import it.
*
* We also had to change the target in tsconfig.json to esnext
*/
config.experiments.topLevelAwait = true;

return config;
},
productionBrowserSourceMaps: true,
};

module.exports = nextConfig;
40 changes: 40 additions & 0 deletions patches/babel-plugin-macros+3.1.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
diff --git a/node_modules/babel-plugin-macros/dist/index.js b/node_modules/babel-plugin-macros/dist/index.js
index 4501c0e..1d347ed 100644
--- a/node_modules/babel-plugin-macros/dist/index.js
+++ b/node_modules/babel-plugin-macros/dist/index.js
@@ -252,19 +252,25 @@ function applyMacros({
}

function getConfigFromFile(configName, filename) {
- try {
- const loaded = getConfigExplorer().search(filename);
-
- if (loaded) {
+ // only search files for the config if we are running on the server.
+ // in the browser we can't use the filesystem.
+ if (typeof window !== 'undefined') {
+ return {};
+ } else {
+ try {
+ const loaded = getConfigExplorer().search(filename);
+
+ if (loaded) {
+ return {
+ options: loaded.config[configName],
+ path: loaded.filepath
+ };
+ }
+ } catch (e) {
return {
- options: loaded.config[configName],
- path: loaded.filepath
+ error: e
};
}
- } catch (e) {
- return {
- error: e
- };
}

return {};
15 changes: 15 additions & 0 deletions patches/twin.macro+2.8.2.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/node_modules/twin.macro/macro.js b/node_modules/twin.macro/macro.js
index 15e5510..898c8a4 100644
--- a/node_modules/twin.macro/macro.js
+++ b/node_modules/twin.macro/macro.js
@@ -3834,7 +3834,9 @@ var getConfigTailwindProperties = function (state, config) {
var sourceRoot = state.file.opts.sourceRoot || '.';
var configFile = config && config.config;
var configPath = path.resolve(sourceRoot, configFile || "./tailwind.config.js");
- var configExists = fs.existsSync(configPath);
+ // only access the file system if we are running on the server
+ // in the browser the filesystem does not exist so this would throw an error
+ var configExists = typeof window === 'undefined' ? fs.existsSync(configPath) : false;
var path$$1 = configExists ? require(configPath) : defaultTailwindConfig;
var configTailwind = resolveTailwindConfig([].concat( getAllConfigs(path$$1) ));
throwIf(!configTailwind, function () { return logGeneralError(("Couldn’t find the Tailwind config.\nLooked in " + config)); });

1 comment on commit f200a98

@vercel
Copy link

@vercel vercel bot commented on f200a98 Mar 24, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.