diff --git a/styled-components/.eslintrc.js b/styled-components/.eslintrc.js
index 2061cd22..3f74a67d 100644
--- a/styled-components/.eslintrc.js
+++ b/styled-components/.eslintrc.js
@@ -1,4 +1,84 @@
+/**
+ * This is intended to be a basic starting point for linting in your app.
+ * It relies on recommended configs out of the box for simplicity, but you can
+ * and should modify this configuration to best suit your team's needs.
+ */
+
/** @type {import('eslint').Linter.Config} */
module.exports = {
- extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
-};
+ root: true,
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ env: {
+ browser: true,
+ commonjs: true,
+ es6: true,
+ },
+ ignorePatterns: ["!**/.server", "!**/.client"],
+
+ // Base config
+ extends: ["eslint:recommended"],
+
+ overrides: [
+ // React
+ {
+ files: ["**/*.{js,jsx,ts,tsx}"],
+ plugins: ["react", "jsx-a11y"],
+ extends: [
+ "plugin:react/recommended",
+ "plugin:react/jsx-runtime",
+ "plugin:react-hooks/recommended",
+ "plugin:jsx-a11y/recommended",
+ ],
+ settings: {
+ react: {
+ version: "detect",
+ },
+ formComponents: ["Form"],
+ linkComponents: [
+ { name: "Link", linkAttribute: "to" },
+ { name: "NavLink", linkAttribute: "to" },
+ ],
+ "import/resolver": {
+ typescript: {},
+ },
+ },
+ },
+
+ // Typescript
+ {
+ files: ["**/*.{ts,tsx}"],
+ plugins: ["@typescript-eslint", "import"],
+ parser: "@typescript-eslint/parser",
+ settings: {
+ "import/internal-regex": "^~/",
+ "import/resolver": {
+ node: {
+ extensions: [".ts", ".tsx"],
+ },
+ typescript: {
+ alwaysTryTypes: true,
+ },
+ },
+ },
+ extends: [
+ "plugin:@typescript-eslint/recommended",
+ "plugin:import/recommended",
+ "plugin:import/typescript",
+ ],
+ },
+
+ // Node
+ {
+ files: [".eslintrc.cjs"],
+ env: {
+ node: true,
+ },
+ },
+ ],
+};
\ No newline at end of file
diff --git a/styled-components/app/entry.server.tsx b/styled-components/app/entry.server.tsx
index 522730b3..50d35c9c 100644
--- a/styled-components/app/entry.server.tsx
+++ b/styled-components/app/entry.server.tsx
@@ -1,30 +1,99 @@
+import { PassThrough, Transform } from "stream";
+
import type { AppLoadContext, EntryContext } from "@remix-run/node";
+import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
-import { renderToString } from "react-dom/server";
-import { ServerStyleSheet } from "styled-components";
+import { renderToPipeableStream } from "react-dom/server";
+import { ServerStyleSheet, StyleSheetManager } from "styled-components";
+import { isbot } from "isbot";
+
+// Reject/cancel all pending promises after 5 seconds
+export const streamTimeout = 15000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
- loadContext: AppLoadContext,
+ // This is ignored so we can keep it in the template for visibility. Feel
+ // free to delete this parameter in your app if you're not using it!
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ loadContext: AppLoadContext
) {
- const sheet = new ServerStyleSheet();
+ const styleSheet = new ServerStyleSheet();
+ const decoder = new TextDecoder("utf-8");
+ // Stream interceptor in order to inject additional HTML on the fly
+ const transformer = transformStream({ decoder, styleSheet });
+
+ const callbackName = isbot(request.headers.get("user-agent"))
+ ? "onAllReady"
+ : "onShellReady";
+
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async (resolve, reject) => {
+ let didError = false;
- let markup = renderToString(
- sheet.collectStyles(
- ,
- ),
- );
- const styles = sheet.getStyleTags();
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough();
+ responseHeaders.set("Content-Type", "text/html");
- markup = markup.replace("__STYLES__", styles);
+ pipe(transformer);
+ transformer.pipe(body);
- responseHeaders.set("Content-Type", "text/html");
+ resolve(
+ new Response(createReadableStreamFromReadable(body), {
+ status: didError ? 500 : responseStatusCode,
+ headers: responseHeaders,
+ })
+ );
+ },
+ onShellError: (err: unknown) => {
+ reject(err);
+ },
+ onError: () => {
+ didError = true;
+ },
+ }
+ );
- return new Response("" + markup, {
- status: responseStatusCode,
- headers: responseHeaders,
+ // Automatically timeout the React renderer after 6 seconds, which ensures
+ // React has enough time to flush down the rejected boundary contents
+ setTimeout(abort, streamTimeout + 1000);
});
}
+
+/**
+ * Returns a Transform stream that injects styled-components styles into streamed HTML.
+ * - Replaces `__STYLES__` with styled-components CSS.
+ */
+const transformStream = ({
+ decoder,
+ styleSheet,
+}: {
+ decoder: TextDecoder;
+ styleSheet: ServerStyleSheet;
+}) =>
+ new Transform({
+ objectMode: true,
+ flush(callback) {
+ callback();
+ },
+ transform(chunk, encoding, callback) {
+ let renderedHtml =
+ chunk instanceof Uint8Array
+ ? decoder.decode(chunk, { stream: true })
+ : chunk.toString(encoding || "utf8");
+ renderedHtml = renderedHtml.replace(
+ "__STYLES__",
+ styleSheet.getStyleTags()
+ );
+ this.push(renderedHtml);
+
+ callback();
+ },
+ });
diff --git a/styled-components/app/root.tsx b/styled-components/app/root.tsx
index 56e4f56a..9ee3e071 100644
--- a/styled-components/app/root.tsx
+++ b/styled-components/app/root.tsx
@@ -8,11 +8,13 @@ import {
ScrollRestoration,
} from "@remix-run/react";
-export const meta: MetaFunction = () => ({
- charset: "utf-8",
- title: "New Remix App",
- viewport: "width=device-width,initial-scale=1",
-});
+export const meta: MetaFunction = () => [
+ {
+ charset: "utf-8",
+ title: "New Remix App",
+ viewport: "width=device-width,initial-scale=1",
+ },
+];
export default function App() {
return (
@@ -20,6 +22,7 @@ export default function App() {
+ {/* __STYLES__ will be replaced in entry-server.tsx */}
{typeof document === "undefined" ? "__STYLES__" : null}
diff --git a/styled-components/app/routes/_boundary.tsx b/styled-components/app/routes/_boundary.tsx
index c9f5d55d..c059b71a 100644
--- a/styled-components/app/routes/_boundary.tsx
+++ b/styled-components/app/routes/_boundary.tsx
@@ -1,30 +1,35 @@
-import { Outlet, useCatch } from "@remix-run/react";
-
+import { isRouteErrorResponse, Outlet, useRouteError } from "@remix-run/react";
import { Box } from "~/components/Box";
export default function Boundary() {
return ;
}
-export function CatchBoundary() {
- const caught = useCatch();
+export function ErrorBoundary() {
+ const error = useRouteError();
- return (
-
- Catch Boundary
-
- {caught.status} {caught.statusText}
-
-
- );
-}
+ if (isRouteErrorResponse(error)) {
+ return (
+
+ Catch Boundary
+
+ {error.status} {error.statusText}
+
+
+ );
+ }
+
+ let message, stack;
+ if (error instanceof Error) {
+ message = error.message;
+ stack = error.stack;
+ }
-export function ErrorBoundary({ error }: { error: Error }) {
return (
Error Boundary
- {error.message}
- {error.stack}
+ {message}
+ {stack}
);
-}
+}
\ No newline at end of file
diff --git a/styled-components/app/styles-context.tsx b/styled-components/app/styles-context.tsx
deleted file mode 100644
index 1010d084..00000000
--- a/styled-components/app/styles-context.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import * as React from "react";
-const StylesContext = React.createContext(null);
-export const StylesProvider = StylesContext.Provider;
-export const useStyles = () => React.useContext(StylesContext);
diff --git a/styled-components/components/src/Box.tsx b/styled-components/components/src/Box.tsx
index 856bb3fb..220b7169 100644
--- a/styled-components/components/src/Box.tsx
+++ b/styled-components/components/src/Box.tsx
@@ -1,4 +1,4 @@
-import styled from "styled-components";
+import { styled } from "styled-components";
export const Box = styled("div")`
font-family: system-ui, sans-serif;
diff --git a/styled-components/components/tsconfig.json b/styled-components/components/tsconfig.json
index 40bbe784..2279024c 100644
--- a/styled-components/components/tsconfig.json
+++ b/styled-components/components/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts", "**/*.tsx"],
+ "exclude": [],
"compilerOptions": {
"outDir": "../app/components",
diff --git a/styled-components/package.json b/styled-components/package.json
index 35ef4d72..52044d66 100644
--- a/styled-components/package.json
+++ b/styled-components/package.json
@@ -1,6 +1,7 @@
{
"private": true,
"sideEffects": false,
+ "type": "module",
"scripts": {
"build": "run-s \"build:*\"",
"build:components": "run-p \"build:components:*\"",
@@ -14,33 +15,43 @@
"dev:remix": "remix dev",
"generate:components:src": "babel components/src --out-dir app/components --extensions .ts,.js,.tsx,.jsx,.cjs,.mjs",
"generate:components:types": "tsc --project components/tsconfig.json",
- "start": "remix-serve build",
+ "start": "remix-serve ./build/index.js",
"typecheck": "tsc"
},
"dependencies": {
- "@remix-run/node": "^1.19.3",
- "@remix-run/react": "^1.19.3",
- "@remix-run/serve": "^1.19.3",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "styled-components": "^5.3.3"
+ "@remix-run/node": "^2.16.6",
+ "@remix-run/react": "^2.16.6",
+ "@remix-run/serve": "^2.16.6",
+ "isbot": "^4.1.0",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "styled-components": "^6.1.18"
},
"devDependencies": {
- "@babel/cli": "^7.22.10",
- "@babel/core": "^7.22.10",
- "@babel/preset-react": "^7.22.5",
- "@babel/preset-typescript": "^7.22.5",
- "@remix-run/dev": "^1.19.3",
- "@remix-run/eslint-config": "^1.19.3",
- "@types/react": "^18.2.20",
- "@types/react-dom": "^18.2.7",
- "@types/styled-components": "^5.1.24",
+ "@babel/cli": "^7.27.2",
+ "@babel/core": "^7.27.1",
+ "@babel/preset-react": "^7.27.1",
+ "@babel/preset-typescript": "^7.27.1",
+ "@remix-run/dev": "^2.16.6",
+ "@types/react": "^19.1.4",
+ "@types/react-dom": "^19.1.5",
+ "@types/styled-components": "^5.1.34",
+ "@typescript-eslint/eslint-plugin": "^6.7.4",
+ "@typescript-eslint/parser": "^6.7.4",
+ "autoprefixer": "^10.4.19",
"babel-plugin-styled-components": "^2.1.4",
- "eslint": "^8.27.0",
+ "eslint": "^8.38.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.28.1",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
"npm-run-all": "^4.1.5",
- "typescript": "^4.8.4"
+ "typescript": "^5.1.6",
+ "vite": "^6.0.0",
+ "vite-tsconfig-paths": "^4.2.1"
},
"engines": {
- "node": ">=14.0.0"
+ "node": ">=20.0.0"
}
}
diff --git a/styled-components/remix.config.js b/styled-components/remix.config.js
deleted file mode 100644
index ca00ba94..00000000
--- a/styled-components/remix.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- future: {
- v2_routeConvention: true,
- },
- ignoredRouteFiles: ["**/.*"],
- // appDirectory: "app",
- // assetsBuildDirectory: "public/build",
- // publicPath: "/build/",
- // serverBuildPath: "build/index.js",
-};
diff --git a/styled-components/remix.env.d.ts b/styled-components/remix.env.d.ts
deleted file mode 100644
index dcf8c45e..00000000
--- a/styled-components/remix.env.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-///
-///
diff --git a/styled-components/tsconfig.json b/styled-components/tsconfig.json
index 20f8a386..e5150036 100644
--- a/styled-components/tsconfig.json
+++ b/styled-components/tsconfig.json
@@ -1,22 +1,22 @@
{
- "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
- "lib": ["DOM", "DOM.Iterable", "ES2019"],
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@remix-run/node", "vite/client"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
- "moduleResolution": "node",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
"resolveJsonModule": true,
- "target": "ES2019",
+ "target": "ES2022",
"strict": true,
"allowJs": true,
+ "skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
- },
-
- // Remix takes care of building everything in `remix build`.
- "noEmit": true
+ }
}
}
diff --git a/styled-components/vite.config.ts b/styled-components/vite.config.ts
new file mode 100644
index 00000000..e4e8cefc
--- /dev/null
+++ b/styled-components/vite.config.ts
@@ -0,0 +1,24 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+declare module "@remix-run/node" {
+ interface Future {
+ v3_singleFetch: true;
+ }
+}
+
+export default defineConfig({
+ plugins: [
+ remix({
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ v3_singleFetch: true,
+ v3_lazyRouteDiscovery: true,
+ },
+ }),
+ tsconfigPaths(),
+ ],
+});