Skip to content

Instantly share code, notes, and snippets.

@jenseng
Last active June 3, 2022 17:24
Show Gist options
  • Save jenseng/7d310702871baf72eb03890fcd35a35a to your computer and use it in GitHub Desktop.
Save jenseng/7d310702871baf72eb03890fcd35a35a to your computer and use it in GitHub Desktop.
highlight route components (remix and create-react-app)

Here's the basic process I followed for implementing the "Show Route Components" feature in the demo apps I showed in my Remix conf talk.

Someone could probably generalize/improve this further and make it into a drop-in library :)

See the files below for the specifics, but here's the TL;DR:

  1. Add some css to make [data-filepath] look how you want
  2. Annotate each route component with data-filepath={__filename}
  3. (optional) Make a toggle component
  4. (optional) Make the path links clickable
  5. Make __filename resolve correctly for CRA (webpack config)
  6. Make __filename resolve correctly for Remix (esbuild hackery)
/* 1. add some css like so */
[data-filepath] {
margin: -2px;
border: 2px solid #400;
position: relative;
box-shadow: 0 0 6px 3px #400;
}
[data-filepath]::before {
content: "◫ " attr(data-filepath) " ⮕";
background: #400;
color: #888;
padding: 0.4em 0.8em;
line-height: 1em;
position: absolute;
top: -1.8em;
right: 0px;
cursor: pointer;
pointer-events: auto;
}
[data-filepath]:hover {
border: 2px solid #800;
box-shadow: 0 0 6px 3px #800;
}
[data-filepath]:hover::before {
background: #800;
color: #fff;
}
// 2. set the file path on each route component's top-most element
return (
<div data-filepath={__filename}>
...
</div>
);
// 3. (optional) make a component to toggle whether or not route components are highlighted (could mount/unmount the css,
// or scope the CSS to a className that gets toggled, etc)
// this is left as an exercise to the reader :)
// 4. (optional) make the filenames clickable. in my case they are `::before` pseudo-elements and not actual links, so i
// added a top-level click handler like so
(e) => {
const el = e.target as HTMLElement;
if (
enabled &&
el.dataset?.filepath &&
e.nativeEvent.offsetY < el.offsetTop
) {
const sourceCodeUrl = `${sourceBaseUrl}${el.dataset.filepath}`;
window.open(sourceCodeUrl, "_blank");
}
}
// 5. for create-react-app, use craco (or react-app-rewired) to make webpack resolve the `__filename`, e.g. my
// `craco.config.js` looks like:
module.exports = {
webpack: {
configure: (config) => {
config.node.__filename = true;
return config;
},
},
};
// 6. for remix, it's a little more involved since 1. esbuild doesn't have this baked in and 2. you can't easily get at
// remix' esbuild config, so:
// 6.1. grab filenamePlugin.js below
// 6.2. add `filenamePlugin` to your remix esbuild plugins and wrap any other plugins with `filenameInterceptor`
// you could do this with patch-package, or with an esbuild-overrides.js as explained here:
// https://github.com/remix-run/remix/issues/717#issuecomment-1026030797
// for example, the overridden `build` function in esbuild-overrides.js would look like this:
const build = (options) => {
return originalBuild({
...options,
plugins: [
// hook into existing plugins to make sure any loaded file contents get replaced
...options.plugins.map(filenameInterceptor),
// add our plugin to catch any other files and replace their contents
filenamePlugin,
],
});
};
const fs = require("fs");
const { getLoaderForFile } = require("@remix-run/dev/compiler/loaders");
/** esbuild plugin to rewrite __filename in any files not already handled by another plugin */
const filenamePlugin = {
name: "filename",
setup(build) {
build.onLoad({ filter: /.*/ }, async ({ path: filepath }) => {
if (isAppFile(filepath)) {
let contents = await fs.promises.readFile(filepath, "utf8");
contents = replaceFilename(contents, filepath);
const loader = getLoaderForFile(filepath).substring(1);
return { contents, loader };
}
});
},
};
/** wrap another esbuild plugin to rewrite __filename in its onLoad contents (if any) */
function filenameInterceptor({ name, setup }) {
return {
name,
setup(build) {
setup(
new Proxy(build, {
get(target, property) {
if (property === "onLoad") {
return (options, callback) => {
build.onLoad(options, async (args) => {
const result = await callback(args);
if (result.contents && isAppFile(args.path)) {
result.contents = replaceFilename(
result.contents,
args.path
);
}
return result;
});
};
} else {
return target[property];
}
},
})
);
},
};
}
module.exports = { filenamePlugin, filenameInterceptor };
const cwd = process.cwd() + "/";
function isAppFile(filename) {
return !filename.match(/node_modules/) && filename.startsWith(cwd);
}
function replaceFilename(contents, filename) {
// relativize it
filename = filename.replace(cwd, "");
return contents.replace("__filename", `"${filename}"`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment