├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── app ├── entry.client.tsx ├── entry.server.tsx ├── root.tsx ├── routes │ └── index.tsx └── utils │ └── data.ts ├── images └── screenshot-types.png ├── package-lock.json ├── package.json ├── patches └── @remix-run+dev+1.6.5.patch ├── public └── favicon.ico ├── remix.config.js ├── remix.env.d.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "avoid" 6 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2022 kiliman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remix SuperJSON 2 | 3 | This sample shows how to use the [`superjson`](https://www.npmjs.com/package/superjson) package to safely serialize non-JSON data from 4 | your loaders and get the correct types in your route component. 5 | 6 | Thanks to the type inference support added to Remix v1.6.5, you also 7 | get full fidelity type inference. 8 | 9 | Demo: https://remix-superjson-production.up.railway.app 10 | 11 | ```ts 12 | export async function loader({ request }: LoaderArgs) { 13 | var url = new URL(request.url) 14 | if (url.searchParams.has('redirect')) { 15 | return redirect('/', { headers: { 'set-cookie': 'superjson=true' } }) 16 | } 17 | return superjson( 18 | { greeting: 'hello', today: new Date() }, 19 | { headers: { 'x-superjson': 'true' } }, 20 | ) 21 | } 22 | export default function Index() { 23 | const data = useSuperLoaderData() 24 | return

Today is {data.today.toLocaleString()}

25 | } 26 | ``` 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from "@remix-run/react"; 2 | import { hydrate } from "react-dom"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { EntryContext } from "@remix-run/node"; 2 | import { RemixServer } from "@remix-run/react"; 3 | import { renderToString } from "react-dom/server"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | let markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set("Content-Type", "text/html"); 16 | 17 | return new Response("" + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from '@remix-run/node' 2 | import { 3 | Links, 4 | LiveReload, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration, 9 | } from '@remix-run/react' 10 | 11 | export const meta: MetaFunction = () => ({ 12 | charset: 'utf-8', 13 | title: 'Remix SuperJSON', 14 | viewport: 'width=device-width,initial-scale=1', 15 | }) 16 | 17 | export default function App() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderArgs } from '@remix-run/node' 2 | import { redirect, superjson, useSuperLoaderData } from '~/utils/data' 3 | 4 | export async function loader({ request }: LoaderArgs) { 5 | var url = new URL(request.url) 6 | if (url.searchParams.has('redirect')) { 7 | return redirect('/', { headers: { 'set-cookie': 'superjson=true' } }) 8 | } 9 | return superjson( 10 | { greeting: 'hello', today: new Date() }, 11 | { headers: { 'x-superjson': 'true' } }, 12 | ) 13 | } 14 | 15 | export default function Index() { 16 | const data = useSuperLoaderData() 17 | 18 | return ( 19 |
20 |

Remix SuperJSON

21 |
{JSON.stringify(data, null, 2)}
22 |

Today is {data.today.toLocaleDateString()}

23 |

24 | GitHub 25 |

26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/utils/data.ts: -------------------------------------------------------------------------------- 1 | import { useActionData, useLoaderData } from '@remix-run/react' 2 | import * as _superjson from 'superjson' 3 | 4 | export type SuperJsonFunction = ( 5 | data: Data, 6 | init?: number | ResponseInit, 7 | ) => SuperTypedResponse 8 | 9 | export declare type SuperTypedResponse = 10 | Response & { 11 | superjson(): Promise 12 | } 13 | 14 | type AppData = any 15 | type DataFunction = (...args: any[]) => unknown // matches any function 16 | type DataOrFunction = AppData | DataFunction 17 | 18 | export type UseDataFunctionReturn = T extends ( 19 | ...args: any[] 20 | ) => infer Output 21 | ? Awaited extends SuperTypedResponse 22 | ? U 23 | : Awaited> 24 | : Awaited 25 | 26 | export const superjson: SuperJsonFunction = (data, init = {}) => { 27 | let responseInit = typeof init === 'number' ? { status: init } : init 28 | let headers = new Headers(responseInit.headers) 29 | if (!headers.has('Content-Type')) { 30 | headers.set('Content-Type', 'application/json; charset=utf-8') 31 | } 32 | return new Response(_superjson.stringify(data), { 33 | ...responseInit, 34 | headers, 35 | }) as SuperTypedResponse 36 | } 37 | 38 | export function useSuperLoaderData(): UseDataFunctionReturn { 39 | const data = useLoaderData() 40 | return _superjson.deserialize(data) 41 | } 42 | export function useSuperActionData< 43 | T = AppData, 44 | >(): UseDataFunctionReturn | null { 45 | const data = useActionData() 46 | return data ? _superjson.deserialize(data) : null 47 | } 48 | 49 | export type RedirectFunction = ( 50 | url: string, 51 | init?: number | ResponseInit, 52 | ) => SuperTypedResponse 53 | 54 | /** 55 | * A redirect response. Sets the status code and the `Location` header. 56 | * Defaults to "302 Found". 57 | * 58 | * @see https://remix.run/api/remix#redirect 59 | */ 60 | export const redirect: RedirectFunction = (url, init = 302) => { 61 | let responseInit = init 62 | if (typeof responseInit === 'number') { 63 | responseInit = { status: responseInit } 64 | } else if (typeof responseInit.status === 'undefined') { 65 | responseInit.status = 302 66 | } 67 | 68 | let headers = new Headers(responseInit.headers) 69 | headers.set('Location', url) 70 | 71 | return new Response(null, { 72 | ...responseInit, 73 | headers, 74 | }) as SuperTypedResponse 75 | } 76 | -------------------------------------------------------------------------------- /images/screenshot-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiliman/remix-superjson/d6593dd2e15c5349274d02a7b9fecc993f03867d/images/screenshot-types.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "sideEffects": false, 4 | "scripts": { 5 | "build": "remix build", 6 | "dev": "remix dev", 7 | "start": "remix-serve build" 8 | }, 9 | "dependencies": { 10 | "@remix-run/node": "^1.6.5", 11 | "@remix-run/react": "^1.6.5", 12 | "@remix-run/serve": "^1.6.5", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "superjson": "^1.9.1" 16 | }, 17 | "devDependencies": { 18 | "@remix-run/dev": "^1.6.5", 19 | "@remix-run/eslint-config": "^1.6.5", 20 | "@types/react": "^17.0.45", 21 | "@types/react-dom": "^17.0.17", 22 | "eslint": "^8.15.0", 23 | "patch-package": "^6.4.7", 24 | "prettier": "^2.7.1", 25 | "typescript": "^4.7.4" 26 | }, 27 | "engines": { 28 | "node": ">=14" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /patches/@remix-run+dev+1.6.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@remix-run/dev/dist/compiler.js b/node_modules/@remix-run/dev/dist/compiler.js 2 | index 9d39471..63763ed 100644 3 | --- a/node_modules/@remix-run/dev/dist/compiler.js 4 | +++ b/node_modules/@remix-run/dev/dist/compiler.js 5 | @@ -376,6 +376,7 @@ function createServerBuild(config, options, assetsManifestPromiseRef) { 6 | platform: config.serverPlatform, 7 | format: config.serverModuleFormat, 8 | treeShaking: true, 9 | + metafile: true, 10 | // The type of dead code elimination we want to do depends on the 11 | // minify syntax property: https://github.com/evanw/esbuild/issues/672#issuecomment-1029682369 12 | // Dev builds are leaving code that should be optimized away in the 13 | @@ -405,6 +406,12 @@ function createServerBuild(config, options, assetsManifestPromiseRef) { 14 | plugins 15 | }).then(async build => { 16 | await writeServerBuildResult(config, build.outputFiles); 17 | + let analysis = await esbuild.analyzeMetafile(build.metafile, { 18 | + verbose: true, 19 | + }) 20 | + var serverBuildDirectory = path__namespace.dirname(config.serverBuildPath) 21 | + await fs.writeFileSafe(path__namespace.join(serverBuildDirectory, 'meta.json'), JSON.stringify(build.metafile, null, 2)); 22 | + await fs.writeFileSafe(path__namespace.join(serverBuildDirectory, 'bundle-analysis.txt'), analysis); 23 | return build; 24 | }); 25 | } 26 | @@ -414,6 +421,11 @@ async function generateAssetsManifest(config, metafile) { 27 | let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; 28 | assetsManifest.url = config.publicPath + filename; 29 | await fs.writeFileSafe(path__namespace.join(config.assetsBuildDirectory, filename), `window.__remixManifest=${JSON.stringify(assetsManifest)};`); 30 | + let analysis = await esbuild.analyzeMetafile(metafile, { 31 | + verbose: true, 32 | + }) 33 | + await fs.writeFileSafe(path__namespace.join(config.assetsBuildDirectory, 'meta.json'), JSON.stringify(metafile, null, 2)); 34 | + await fs.writeFileSafe(path__namespace.join(config.assetsBuildDirectory, 'bundle-analysis.txt'), analysis); 35 | return assetsManifest; 36 | } 37 | 38 | diff --git a/node_modules/@remix-run/dev/dist/compiler.js.orig b/node_modules/@remix-run/dev/dist/compiler.js.orig 39 | new file mode 100644 40 | index 0000000..9d39471 41 | --- /dev/null 42 | +++ b/node_modules/@remix-run/dev/dist/compiler.js.orig 43 | @@ -0,0 +1,443 @@ 44 | +/** 45 | + * @remix-run/dev v1.6.5 46 | + * 47 | + * Copyright (c) Remix Software Inc. 48 | + * 49 | + * This source code is licensed under the MIT license found in the 50 | + * LICENSE.md file in the root directory of this source tree. 51 | + * 52 | + * @license MIT 53 | + */ 54 | +'use strict'; 55 | + 56 | +Object.defineProperty(exports, '__esModule', { value: true }); 57 | + 58 | +var path = require('path'); 59 | +var module$1 = require('module'); 60 | +var esbuild = require('esbuild'); 61 | +var fse = require('fs-extra'); 62 | +var debounce = require('lodash.debounce'); 63 | +var chokidar = require('chokidar'); 64 | +var nodeModulesPolyfill = require('@esbuild-plugins/node-modules-polyfill'); 65 | +var esbuildPluginPnp = require('@yarnpkg/esbuild-plugin-pnp'); 66 | +var build$1 = require('./build.js'); 67 | +var config = require('./config.js'); 68 | +var warnings = require('./compiler/warnings.js'); 69 | +var assets = require('./compiler/assets.js'); 70 | +var dependencies = require('./compiler/dependencies.js'); 71 | +var loaders = require('./compiler/loaders.js'); 72 | +var browserRouteModulesPlugin = require('./compiler/plugins/browserRouteModulesPlugin.js'); 73 | +var emptyModulesPlugin = require('./compiler/plugins/emptyModulesPlugin.js'); 74 | +var mdx = require('./compiler/plugins/mdx.js'); 75 | +var serverAssetsManifestPlugin = require('./compiler/plugins/serverAssetsManifestPlugin.js'); 76 | +var serverBareModulesPlugin = require('./compiler/plugins/serverBareModulesPlugin.js'); 77 | +var serverEntryModulePlugin = require('./compiler/plugins/serverEntryModulePlugin.js'); 78 | +var serverRouteModulesPlugin = require('./compiler/plugins/serverRouteModulesPlugin.js'); 79 | +var fs = require('./compiler/utils/fs.js'); 80 | +var urlImportsPlugin = require('./compiler/plugins/urlImportsPlugin.js'); 81 | + 82 | +function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } 83 | + 84 | +function _interopNamespace(e) { 85 | + if (e && e.__esModule) return e; 86 | + var n = Object.create(null); 87 | + if (e) { 88 | + Object.keys(e).forEach(function (k) { 89 | + if (k !== 'default') { 90 | + var d = Object.getOwnPropertyDescriptor(e, k); 91 | + Object.defineProperty(n, k, d.get ? d : { 92 | + enumerable: true, 93 | + get: function () { return e[k]; } 94 | + }); 95 | + } 96 | + }); 97 | + } 98 | + n["default"] = e; 99 | + return Object.freeze(n); 100 | +} 101 | + 102 | +var path__namespace = /*#__PURE__*/_interopNamespace(path); 103 | +var esbuild__namespace = /*#__PURE__*/_interopNamespace(esbuild); 104 | +var fse__namespace = /*#__PURE__*/_interopNamespace(fse); 105 | +var debounce__default = /*#__PURE__*/_interopDefaultLegacy(debounce); 106 | +var chokidar__default = /*#__PURE__*/_interopDefaultLegacy(chokidar); 107 | + 108 | +// directory in the same place relative to this file. It is eventually injected 109 | +// as a source file when building the app. 110 | + 111 | +const reactShim = path__namespace.resolve(__dirname, "compiler/shims/react.ts"); 112 | + 113 | +function defaultWarningHandler(message, key) { 114 | + warnings.warnOnce(message, key); 115 | +} 116 | + 117 | +function defaultBuildFailureHandler(failure) { 118 | + formatBuildFailure(failure); 119 | +} 120 | + 121 | +function formatBuildFailure(failure) { 122 | + if ("warnings" in failure || "errors" in failure) { 123 | + if (failure.warnings) { 124 | + let messages = esbuild__namespace.formatMessagesSync(failure.warnings, { 125 | + kind: "warning", 126 | + color: true 127 | + }); 128 | + console.warn(...messages); 129 | + } 130 | + 131 | + if (failure.errors) { 132 | + let messages = esbuild__namespace.formatMessagesSync(failure.errors, { 133 | + kind: "error", 134 | + color: true 135 | + }); 136 | + console.error(...messages); 137 | + } 138 | + } 139 | + 140 | + console.error((failure === null || failure === void 0 ? void 0 : failure.message) || "An unknown build error occurred"); 141 | +} 142 | +async function build(config, { 143 | + mode = build$1.BuildMode.Production, 144 | + target = build$1.BuildTarget.Node14, 145 | + sourcemap = false, 146 | + onWarning = defaultWarningHandler, 147 | + onBuildFailure = defaultBuildFailureHandler 148 | +} = {}) { 149 | + let assetsManifestPromiseRef = {}; 150 | + await buildEverything(config, assetsManifestPromiseRef, { 151 | + mode, 152 | + target, 153 | + sourcemap, 154 | + onWarning, 155 | + onBuildFailure 156 | + }); 157 | +} 158 | +async function watch(config$1, { 159 | + mode = build$1.BuildMode.Development, 160 | + target = build$1.BuildTarget.Node14, 161 | + sourcemap = true, 162 | + onWarning = defaultWarningHandler, 163 | + onBuildFailure = defaultBuildFailureHandler, 164 | + onRebuildStart, 165 | + onRebuildFinish, 166 | + onFileCreated, 167 | + onFileChanged, 168 | + onFileDeleted, 169 | + onInitialBuild 170 | +} = {}) { 171 | + var _config$watchPaths; 172 | + 173 | + let options = { 174 | + mode, 175 | + target, 176 | + sourcemap, 177 | + onBuildFailure, 178 | + onWarning, 179 | + incremental: true 180 | + }; 181 | + let assetsManifestPromiseRef = {}; 182 | + let [browserBuild, serverBuild] = await buildEverything(config$1, assetsManifestPromiseRef, options); 183 | + let initialBuildComplete = !!browserBuild && !!serverBuild; 184 | + 185 | + if (initialBuildComplete) { 186 | + onInitialBuild === null || onInitialBuild === void 0 ? void 0 : onInitialBuild(); 187 | + } 188 | + 189 | + function disposeBuilders() { 190 | + var _browserBuild, _browserBuild$rebuild, _serverBuild, _serverBuild$rebuild; 191 | + 192 | + (_browserBuild = browserBuild) === null || _browserBuild === void 0 ? void 0 : (_browserBuild$rebuild = _browserBuild.rebuild) === null || _browserBuild$rebuild === void 0 ? void 0 : _browserBuild$rebuild.dispose(); 193 | + (_serverBuild = serverBuild) === null || _serverBuild === void 0 ? void 0 : (_serverBuild$rebuild = _serverBuild.rebuild) === null || _serverBuild$rebuild === void 0 ? void 0 : _serverBuild$rebuild.dispose(); 194 | + browserBuild = undefined; 195 | + serverBuild = undefined; 196 | + } 197 | + 198 | + let restartBuilders = debounce__default["default"](async newConfig => { 199 | + disposeBuilders(); 200 | + 201 | + try { 202 | + newConfig = await config.readConfig(config$1.rootDirectory); 203 | + } catch (error) { 204 | + onBuildFailure(error); 205 | + return; 206 | + } 207 | + 208 | + config$1 = newConfig; 209 | + if (onRebuildStart) onRebuildStart(); 210 | + let builders = await buildEverything(config$1, assetsManifestPromiseRef, options); 211 | + if (onRebuildFinish) onRebuildFinish(); 212 | + browserBuild = builders[0]; 213 | + serverBuild = builders[1]; 214 | + }, 500); 215 | + let rebuildEverything = debounce__default["default"](async () => { 216 | + var _browserBuild2, _serverBuild2; 217 | + 218 | + if (onRebuildStart) onRebuildStart(); 219 | + 220 | + if (!((_browserBuild2 = browserBuild) !== null && _browserBuild2 !== void 0 && _browserBuild2.rebuild) || !((_serverBuild2 = serverBuild) !== null && _serverBuild2 !== void 0 && _serverBuild2.rebuild)) { 221 | + disposeBuilders(); 222 | + 223 | + try { 224 | + [browserBuild, serverBuild] = await buildEverything(config$1, assetsManifestPromiseRef, options); 225 | + 226 | + if (!initialBuildComplete) { 227 | + initialBuildComplete = !!browserBuild && !!serverBuild; 228 | + 229 | + if (initialBuildComplete) { 230 | + onInitialBuild === null || onInitialBuild === void 0 ? void 0 : onInitialBuild(); 231 | + } 232 | + } 233 | + 234 | + if (onRebuildFinish) onRebuildFinish(); 235 | + } catch (err) { 236 | + onBuildFailure(err); 237 | + } 238 | + 239 | + return; 240 | + } // If we get here and can't call rebuild something went wrong and we 241 | + // should probably blow as it's not really recoverable. 242 | + 243 | + 244 | + let browserBuildPromise = browserBuild.rebuild(); 245 | + let assetsManifestPromise = browserBuildPromise.then(build => generateAssetsManifest(config$1, build.metafile)); // Assign the assetsManifestPromise to a ref so the server build can await 246 | + // it when loading the @remix-run/dev/assets-manifest virtual module. 247 | + 248 | + assetsManifestPromiseRef.current = assetsManifestPromise; 249 | + await Promise.all([assetsManifestPromise, serverBuild.rebuild().then(build => writeServerBuildResult(config$1, build.outputFiles))]).catch(err => { 250 | + disposeBuilders(); 251 | + onBuildFailure(err); 252 | + }); 253 | + if (onRebuildFinish) onRebuildFinish(); 254 | + }, 100); 255 | + let toWatch = [config$1.appDirectory]; 256 | + 257 | + if (config$1.serverEntryPoint) { 258 | + toWatch.push(config$1.serverEntryPoint); 259 | + } 260 | + 261 | + (_config$watchPaths = config$1.watchPaths) === null || _config$watchPaths === void 0 ? void 0 : _config$watchPaths.forEach(watchPath => { 262 | + toWatch.push(watchPath); 263 | + }); 264 | + let watcher = chokidar__default["default"].watch(toWatch, { 265 | + persistent: true, 266 | + ignoreInitial: true, 267 | + awaitWriteFinish: { 268 | + stabilityThreshold: 100, 269 | + pollInterval: 100 270 | + } 271 | + }).on("error", error => console.error(error)).on("change", async file => { 272 | + if (onFileChanged) onFileChanged(file); 273 | + await rebuildEverything(); 274 | + }).on("add", async file => { 275 | + if (onFileCreated) onFileCreated(file); 276 | + let newConfig; 277 | + 278 | + try { 279 | + newConfig = await config.readConfig(config$1.rootDirectory); 280 | + } catch (error) { 281 | + onBuildFailure(error); 282 | + return; 283 | + } 284 | + 285 | + if (isEntryPoint(newConfig, file)) { 286 | + await restartBuilders(newConfig); 287 | + } else { 288 | + await rebuildEverything(); 289 | + } 290 | + }).on("unlink", async file => { 291 | + if (onFileDeleted) onFileDeleted(file); 292 | + 293 | + if (isEntryPoint(config$1, file)) { 294 | + await restartBuilders(); 295 | + } else { 296 | + await rebuildEverything(); 297 | + } 298 | + }); 299 | + return async () => { 300 | + await watcher.close().catch(() => {}); 301 | + disposeBuilders(); 302 | + }; 303 | +} 304 | + 305 | +function isEntryPoint(config, file) { 306 | + let appFile = path__namespace.relative(config.appDirectory, file); 307 | + 308 | + if (appFile === config.entryClientFile || appFile === config.entryServerFile) { 309 | + return true; 310 | + } 311 | + 312 | + for (let key in config.routes) { 313 | + if (appFile === config.routes[key].file) return true; 314 | + } 315 | + 316 | + return false; 317 | +} /////////////////////////////////////////////////////////////////////////////// 318 | + 319 | + 320 | +async function buildEverything(config, assetsManifestPromiseRef, options) { 321 | + try { 322 | + let browserBuildPromise = createBrowserBuild(config, options); 323 | + let assetsManifestPromise = browserBuildPromise.then(build => generateAssetsManifest(config, build.metafile)); // Assign the assetsManifestPromise to a ref so the server build can await 324 | + // it when loading the @remix-run/dev/assets-manifest virtual module. 325 | + 326 | + assetsManifestPromiseRef.current = assetsManifestPromise; 327 | + let serverBuildPromise = createServerBuild(config, options, assetsManifestPromiseRef); 328 | + return await Promise.all([assetsManifestPromise.then(() => browserBuildPromise), serverBuildPromise]); 329 | + } catch (err) { 330 | + options.onBuildFailure(err); 331 | + return [undefined, undefined]; 332 | + } 333 | +} 334 | + 335 | +async function createBrowserBuild(config, options) { 336 | + // For the browser build, exclude node built-ins that don't have a 337 | + // browser-safe alternative installed in node_modules. Nothing should 338 | + // *actually* be external in the browser build (we want to bundle all deps) so 339 | + // this is really just making sure we don't accidentally have any dependencies 340 | + // on node built-ins in browser bundles. 341 | + let dependencies$1 = Object.keys(dependencies.getAppDependencies(config)); 342 | + let externals = module$1.builtinModules.filter(mod => !dependencies$1.includes(mod)); 343 | + let fakeBuiltins = module$1.builtinModules.filter(mod => dependencies$1.includes(mod)); 344 | + 345 | + if (fakeBuiltins.length > 0) { 346 | + throw new Error(`It appears you're using a module that is built in to node, but you installed it as a dependency which could cause problems. Please remove ${fakeBuiltins.join(", ")} before continuing.`); 347 | + } 348 | + 349 | + let entryPoints = { 350 | + "entry.client": path__namespace.resolve(config.appDirectory, config.entryClientFile) 351 | + }; 352 | + 353 | + for (let id of Object.keys(config.routes)) { 354 | + // All route entry points are virtual modules that will be loaded by the 355 | + // browserEntryPointsPlugin. This allows us to tree-shake server-only code 356 | + // that we don't want to run in the browser (i.e. action & loader). 357 | + entryPoints[id] = path__namespace.resolve(config.appDirectory, config.routes[id].file) + "?browser"; 358 | + } 359 | + 360 | + let plugins = [urlImportsPlugin.urlImportsPlugin(), mdx.mdxPlugin(config), browserRouteModulesPlugin.browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin.emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), nodeModulesPolyfill.NodeModulesPolyfillPlugin(), esbuildPluginPnp.pnpPlugin()]; 361 | + return esbuild__namespace.build({ 362 | + entryPoints, 363 | + outdir: config.assetsBuildDirectory, 364 | + platform: "browser", 365 | + format: "esm", 366 | + external: externals, 367 | + inject: config.serverBuildTarget === "deno" ? [] : [reactShim], 368 | + loader: loaders.loaders, 369 | + bundle: true, 370 | + logLevel: "silent", 371 | + splitting: true, 372 | + sourcemap: options.sourcemap, 373 | + metafile: true, 374 | + incremental: options.incremental, 375 | + mainFields: ["browser", "module", "main"], 376 | + treeShaking: true, 377 | + minify: options.mode === build$1.BuildMode.Production, 378 | + entryNames: "[dir]/[name]-[hash]", 379 | + chunkNames: "_shared/[name]-[hash]", 380 | + assetNames: "_assets/[name]-[hash]", 381 | + publicPath: config.publicPath, 382 | + define: { 383 | + "process.env.NODE_ENV": JSON.stringify(options.mode), 384 | + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify(config.devServerPort) 385 | + }, 386 | + plugins 387 | + }); 388 | +} 389 | + 390 | +function createServerBuild(config, options, assetsManifestPromiseRef) { 391 | + let stdin; 392 | + let entryPoints; 393 | + 394 | + if (config.serverEntryPoint) { 395 | + entryPoints = [config.serverEntryPoint]; 396 | + } else { 397 | + stdin = { 398 | + contents: config.serverBuildTargetEntryModule, 399 | + resolveDir: config.rootDirectory, 400 | + loader: "ts" 401 | + }; 402 | + } 403 | + 404 | + let isCloudflareRuntime = ["cloudflare-pages", "cloudflare-workers"].includes(config.serverBuildTarget ?? ""); 405 | + let isDenoRuntime = config.serverBuildTarget === "deno"; 406 | + let plugins = [urlImportsPlugin.urlImportsPlugin(), mdx.mdxPlugin(config), emptyModulesPlugin.emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), serverRouteModulesPlugin.serverRouteModulesPlugin(config), serverEntryModulePlugin.serverEntryModulePlugin(config), serverAssetsManifestPlugin.serverAssetsManifestPlugin(assetsManifestPromiseRef), serverBareModulesPlugin.serverBareModulesPlugin(config, options.onWarning), esbuildPluginPnp.pnpPlugin()]; 407 | + 408 | + if (config.serverPlatform !== "node") { 409 | + plugins.unshift(nodeModulesPolyfill.NodeModulesPolyfillPlugin()); 410 | + } 411 | + 412 | + return esbuild__namespace.build({ 413 | + absWorkingDir: config.rootDirectory, 414 | + stdin, 415 | + entryPoints, 416 | + outfile: config.serverBuildPath, 417 | + write: false, 418 | + conditions: isCloudflareRuntime ? ["worker"] : isDenoRuntime ? ["deno", "worker"] : undefined, 419 | + platform: config.serverPlatform, 420 | + format: config.serverModuleFormat, 421 | + treeShaking: true, 422 | + // The type of dead code elimination we want to do depends on the 423 | + // minify syntax property: https://github.com/evanw/esbuild/issues/672#issuecomment-1029682369 424 | + // Dev builds are leaving code that should be optimized away in the 425 | + // bundle causing server / testing code to be shipped to the browser. 426 | + // These are properly optimized away in prod builds today, and this 427 | + // PR makes dev mode behave closer to production in terms of dead 428 | + // code elimination / tree shaking is concerned. 429 | + minifySyntax: true, 430 | + minify: options.mode === build$1.BuildMode.Production && isCloudflareRuntime, 431 | + mainFields: isCloudflareRuntime ? ["browser", "module", "main"] : config.serverModuleFormat === "esm" ? ["module", "main"] : ["main", "module"], 432 | + target: options.target, 433 | + inject: config.serverBuildTarget === "deno" ? [] : [reactShim], 434 | + loader: loaders.loaders, 435 | + bundle: true, 436 | + logLevel: "silent", 437 | + incremental: options.incremental, 438 | + sourcemap: options.sourcemap, 439 | + // use linked (true) to fix up .map file 440 | + // The server build needs to know how to generate asset URLs for imports 441 | + // of CSS and other files. 442 | + assetNames: "_assets/[name]-[hash]", 443 | + publicPath: config.publicPath, 444 | + define: { 445 | + "process.env.NODE_ENV": JSON.stringify(options.mode), 446 | + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify(config.devServerPort) 447 | + }, 448 | + plugins 449 | + }).then(async build => { 450 | + await writeServerBuildResult(config, build.outputFiles); 451 | + return build; 452 | + }); 453 | +} 454 | + 455 | +async function generateAssetsManifest(config, metafile) { 456 | + let assetsManifest = await assets.createAssetsManifest(config, metafile); 457 | + let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; 458 | + assetsManifest.url = config.publicPath + filename; 459 | + await fs.writeFileSafe(path__namespace.join(config.assetsBuildDirectory, filename), `window.__remixManifest=${JSON.stringify(assetsManifest)};`); 460 | + return assetsManifest; 461 | +} 462 | + 463 | +async function writeServerBuildResult(config, outputFiles) { 464 | + await fse__namespace.ensureDir(path__namespace.dirname(config.serverBuildPath)); 465 | + 466 | + for (let file of outputFiles) { 467 | + if (file.path.endsWith(".js")) { 468 | + // fix sourceMappingURL to be relative to current path instead of /build 469 | + let filename = file.path.substring(file.path.lastIndexOf(path__namespace.sep) + 1); 470 | + let escapedFilename = filename.replace(/\./g, "\\."); 471 | + let pattern = `(//# sourceMappingURL=)(.*)${escapedFilename}`; 472 | + let contents = Buffer.from(file.contents).toString("utf-8"); 473 | + contents = contents.replace(new RegExp(pattern), `$1${filename}`); 474 | + await fse__namespace.writeFile(file.path, contents); 475 | + } else if (file.path.endsWith(".map")) { 476 | + // remove route: prefix from source filenames so breakpoints work 477 | + let contents = Buffer.from(file.contents).toString("utf-8"); 478 | + contents = contents.replace(/"route:/gm, '"'); 479 | + await fse__namespace.writeFile(file.path, contents); 480 | + } 481 | + } 482 | +} 483 | + 484 | +exports.build = build; 485 | +exports.formatBuildFailure = formatBuildFailure; 486 | +exports.watch = watch; 487 | diff --git a/node_modules/@remix-run/dev/dist/compiler.js.rej b/node_modules/@remix-run/dev/dist/compiler.js.rej 488 | new file mode 100644 489 | index 0000000..0124c76 490 | --- /dev/null 491 | +++ b/node_modules/@remix-run/dev/dist/compiler.js.rej 492 | @@ -0,0 +1,16 @@ 493 | +*************** 494 | +*** 371,376 **** 495 | + platform: config.serverPlatform, 496 | + format: config.serverModuleFormat, 497 | + treeShaking: true, 498 | + minify: options.mode === build$1.BuildMode.Production && isCloudflareRuntime, 499 | + mainFields: isCloudflareRuntime ? ["browser", "module", "main"] : config.serverModuleFormat === "esm" ? ["module", "main"] : ["main", "module"], 500 | + target: options.target, 501 | +--- 371,377 ---- 502 | + platform: config.serverPlatform, 503 | + format: config.serverModuleFormat, 504 | + treeShaking: true, 505 | ++ metafile: true, 506 | + minify: options.mode === build$1.BuildMode.Production && isCloudflareRuntime, 507 | + mainFields: isCloudflareRuntime ? ["browser", "module", "main"] : config.serverModuleFormat === "esm" ? ["module", "main"] : ["main", "module"], 508 | + target: options.target, 509 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiliman/remix-superjson/d6593dd2e15c5349274d02a7b9fecc993f03867d/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | module.exports = { 3 | ignoredRouteFiles: ["**/.*"], 4 | // appDirectory: "app", 5 | // assetsBuildDirectory: "public/build", 6 | // serverBuildPath: "build/index.js", 7 | // publicPath: "/build/", 8 | }; 9 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | 19 | // Remix takes care of building everything in `remix build`. 20 | "noEmit": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------