diff --git a/.eslintrc.js b/.eslintrc.js index e41d5ae0..e51eb1b7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,10 @@ const ERROR = 2; /** @type {import('eslint').Linter.Config} */ module.exports = { root: true, - extends: ["@remix-run/eslint-config/internal", "plugin:markdown/recommended"], + extends: [ + "@remix-run/eslint-config/internal", + "plugin:markdown/recommended-legacy", + ], plugins: ["markdown"], settings: { "import/internal-regex": "^~/", diff --git a/.github/workflows/deduplicate-yarn.yml b/.github/workflows/deduplicate-yarn.yml index fac8ae09..0dd5030d 100644 --- a/.github/workflows/deduplicate-yarn.yml +++ b/.github/workflows/deduplicate-yarn.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v4 - name: ⎔ Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: ".nvmrc" cache: "yarn" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 9b411a73..38e9c037 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - name: ⎔ Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: ".nvmrc" cache: "yarn" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7f935801..bf3a84d0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - name: ⎔ Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: ".nvmrc" cache: "yarn" diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml index d0f1c4db..02a4fef7 100644 --- a/.github/workflows/no-response.yml +++ b/.github/workflows/no-response.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 🥺 Handle Ghosting - uses: actions/stale@v8 + uses: actions/stale@v9 with: days-before-close: 21 # don't automatically mark issues/PRs as stale diff --git a/.nvmrc b/.nvmrc index b6a7d89c..b009dfb9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +lts/* diff --git a/README.md b/README.md index 788973c2..bac5e392 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ You can also initialize a new project with any of these examples using the `--te For example: ``` -npx create-remix@latest --template examples/ +npx create-remix@latest --template remix-run/examples/ ``` Enjoy! diff --git a/__template/.eslintrc.cjs b/__template/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/__template/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/__template/.eslintrc.js b/__template/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/__template/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/__template/.gitignore b/__template/.gitignore index 3f7bf98d..80ec311f 100644 --- a/__template/.gitignore +++ b/__template/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/__template/README.md b/__template/README.md index 8b752894..902a734e 100644 --- a/__template/README.md +++ b/__template/README.md @@ -13,6 +13,7 @@ Open this example on [CodeSandbox](https://codesandbox.com): ## Example Describe the example and how it demonstrates solving the problem. Reference any relevant files/dependencies if needed. +If your example needs to modify the `entry.client.tsx` or `entry.server.tsx` files, use `npx remix reveal` to reveal them. ## Related Links diff --git a/__template/app/root.tsx b/__template/app/root.tsx index 927a0f74..e82f26fd 100644 --- a/__template/app/root.tsx +++ b/__template/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/__template/package.json b/__template/package.json index a437590d..ff7abadc 100644 --- a/__template/package.json +++ b/__template/package.json @@ -1,29 +1,40 @@ { + "name": "template", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/__template/remix.config.js b/__template/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/__template/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/__template/remix.env.d.ts b/__template/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/__template/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/__template/tsconfig.json b/__template/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/__template/tsconfig.json +++ b/__template/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/__template/vite.config.ts b/__template/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/__template/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/_official-realtime-app/.eslintrc.cjs b/_official-realtime-app/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/_official-realtime-app/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/_official-realtime-app/.eslintrc.js b/_official-realtime-app/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/_official-realtime-app/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/_official-realtime-app/.gitignore b/_official-realtime-app/.gitignore index b9a1ffe0..80ec311f 100644 --- a/_official-realtime-app/.gitignore +++ b/_official-realtime-app/.gitignore @@ -2,7 +2,4 @@ node_modules /.cache /build -/public/build .env - -app/styles.processed.css diff --git a/_official-realtime-app/app/root.tsx b/_official-realtime-app/app/root.tsx index 6de2a464..3c58ff93 100644 --- a/_official-realtime-app/app/root.tsx +++ b/_official-realtime-app/app/root.tsx @@ -8,32 +8,33 @@ import { useRevalidator, } from "@remix-run/react"; import { useEffect } from "react"; -import { useEventSource } from "remix-utils"; +import { useEventSource } from "remix-utils/sse/react"; import icons from "~/icons.svg"; -import styles from "~/styles.processed.css"; +import styles from "~/styles.css?url"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "Remix Fake Linear Demo", - viewport: "width=device-width,initial-scale=1", -}); +export const meta: MetaFunction = () => [ + { + title: "Remix Fake Linear Demo", + }, +]; export const links: LinksFunction = () => [ { rel: "stylesheet", href: styles }, { rel: "preload", href: icons, as: "image", fetchpriority: "high" }, ]; -export default function App() { - useRealtimeIssuesRevalidation(); +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} @@ -41,6 +42,11 @@ export default function App() { ); } +export default function App() { + useRealtimeIssuesRevalidation(); + return ; +} + function useRealtimeIssuesRevalidation() { const data = useEventSource("/issues-events"); const { revalidate } = useRevalidator(); diff --git a/_official-realtime-app/app/routes/_index.tsx b/_official-realtime-app/app/routes/_index.tsx index ac4e343f..0a40fcbd 100644 --- a/_official-realtime-app/app/routes/_index.tsx +++ b/_official-realtime-app/app/routes/_index.tsx @@ -119,12 +119,12 @@ function StatusMenu({ issue }: { issue: Issue }) { issue.status === 1 ? "text-yellow-500" : issue.status === 2 - ? "text-orange-500" - : issue.status === 3 - ? "text-green-600" - : issue.status === 4 - ? "text-indigo-600" - : "text-gray-300", + ? "text-orange-500" + : issue.status === 3 + ? "text-green-600" + : issue.status === 4 + ? "text-indigo-600" + : "text-gray-300", )} > diff --git a/_official-realtime-app/app/routes/issues-events.tsx b/_official-realtime-app/app/routes/issues-events.tsx index 93c7d2de..682040c3 100644 --- a/_official-realtime-app/app/routes/issues-events.tsx +++ b/_official-realtime-app/app/routes/issues-events.tsx @@ -1,9 +1,9 @@ -import type { LoaderArgs } from "@remix-run/node"; -import { eventStream } from "remix-utils"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { eventStream } from "remix-utils/sse/server"; import { emitter, EVENTS } from "~/events"; -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { return eventStream(request.signal, (send) => { const handler = (message: string) => { send({ data: message }); diff --git a/_official-realtime-app/app/routes/issues.$id.tsx b/_official-realtime-app/app/routes/issues.$id.tsx index 40a8fa18..b5c5c9b8 100644 --- a/_official-realtime-app/app/routes/issues.$id.tsx +++ b/_official-realtime-app/app/routes/issues.$id.tsx @@ -1,11 +1,11 @@ -import type { LoaderArgs, MetaFunction } from "@remix-run/node"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useFetcher, useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; import { getIssue } from "~/data"; -export const loader = async ({ params }: LoaderArgs) => { +export const loader = async ({ params }: LoaderFunctionArgs) => { invariant(params.id, "Missing issue id"); const issue = await getIssue(params.id); if (!issue) { @@ -14,9 +14,11 @@ export const loader = async ({ params }: LoaderArgs) => { return json(issue); }; -export const meta: MetaFunction = ({ data: issue }) => ({ - title: issue?.title || "Not Found", -}); +export const meta: MetaFunction = ({ data: issue }) => [ + { + title: issue?.title || "Not Found", + }, +]; export default function Issue() { const issue = useLoaderData(); diff --git a/_official-realtime-app/app/routes/issues.$id.update.tsx b/_official-realtime-app/app/routes/issues.$id.update.tsx index 58c64aed..feaf7ae6 100644 --- a/_official-realtime-app/app/routes/issues.$id.update.tsx +++ b/_official-realtime-app/app/routes/issues.$id.update.tsx @@ -1,11 +1,11 @@ -import type { ActionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import invariant from "tiny-invariant"; import { updateIssue } from "~/data"; import { emitter, EVENTS } from "~/events"; -export const action = async ({ params, request }: ActionArgs) => { +export const action = async ({ params, request }: ActionFunctionArgs) => { const updates = Object.fromEntries(await request.formData()); invariant(params.id, "Missing issue id"); const result = await updateIssue(params.id, updates); diff --git a/_official-realtime-app/package.json b/_official-realtime-app/package.json index a6366e54..daf97355 100644 --- a/_official-realtime-app/package.json +++ b/_official-realtime-app/package.json @@ -1,40 +1,46 @@ { + "name": "template", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "run-s \"build:*\"", - "build:css": "npm run generate:css -- --minify", - "build:remix": "remix build", - "dev": "run-p \"dev:*\"", - "dev:css": "npm run generate:css -- --watch", - "dev:remix": "remix dev", - "generate:css": "npx tailwindcss -i app/styles.css -o ./app/styles.processed.css", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", "classnames": "^2.3.2", - "compression": "^1.7.4", - "isbot": "^3.6.5", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "remix-utils": "^6.0.0", + "remix-utils": "^7.6.0", "tiny-invariant": "^1.3.1" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "npm-run-all": "^4.1.5", + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10.4.18", + "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", + "postcss": "^8.4.35", "tailwindcss": "^3.3.0", - "typescript": "^4.8.4" + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/_official-realtime-app/postcss.config.js b/_official-realtime-app/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/_official-realtime-app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/_official-realtime-app/remix.config.js b/_official-realtime-app/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/_official-realtime-app/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/_official-realtime-app/remix.env.d.ts b/_official-realtime-app/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/_official-realtime-app/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/_official-realtime-app/tsconfig.json b/_official-realtime-app/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/_official-realtime-app/tsconfig.json +++ b/_official-realtime-app/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/_official-realtime-app/vite.config.ts b/_official-realtime-app/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/_official-realtime-app/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/client-only-components/.eslintrc.cjs b/client-only-components/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/client-only-components/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/client-only-components/.eslintrc.js b/client-only-components/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/client-only-components/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/client-only-components/.gitignore b/client-only-components/.gitignore index 3f7bf98d..80ec311f 100644 --- a/client-only-components/.gitignore +++ b/client-only-components/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/client-only-components/README.md b/client-only-components/README.md index b85573b1..cd56baa8 100644 --- a/client-only-components/README.md +++ b/client-only-components/README.md @@ -12,8 +12,6 @@ Also shows how to know JS loaded and enable a button that needs JS to work. Open this example on [CodeSandbox](https://codesandbox.com): - - [![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/client-only-components) ## Example diff --git a/client-only-components/app/root.tsx b/client-only-components/app/root.tsx index 927a0f74..e82f26fd 100644 --- a/client-only-components/app/root.tsx +++ b/client-only-components/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/client-only-components/app/routes/_index.tsx b/client-only-components/app/routes/_index.tsx index ff11a4e8..da9b677e 100644 --- a/client-only-components/app/routes/_index.tsx +++ b/client-only-components/app/routes/_index.tsx @@ -1,4 +1,5 @@ -import { ClientOnly, useHydrated } from "remix-utils"; +import { ClientOnly } from "remix-utils/client-only"; +import { useHydrated } from "remix-utils/use-hydrated"; import { BrokenOnTheServer } from "~/components/broken-on-the-server.client"; import { ComplexComponent } from "~/components/complex-component"; diff --git a/client-only-components/package.json b/client-only-components/package.json index 96619c8d..9955808d 100644 --- a/client-only-components/package.json +++ b/client-only-components/package.json @@ -1,30 +1,41 @@ { + "name": "template", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "remix-utils": "^6.0.0" + "remix-utils": "^7.6.0" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/client-only-components/remix.config.js b/client-only-components/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/client-only-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/client-only-components/remix.env.d.ts b/client-only-components/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/client-only-components/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/client-only-components/tsconfig.json b/client-only-components/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/client-only-components/tsconfig.json +++ b/client-only-components/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/client-only-components/vite.config.ts b/client-only-components/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/client-only-components/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/dark-mode/app/routes/action.set-theme.tsx b/dark-mode/app/routes/action.set-theme.tsx index 14d2f393..f8c5fbeb 100644 --- a/dark-mode/app/routes/action.set-theme.tsx +++ b/dark-mode/app/routes/action.set-theme.tsx @@ -1,10 +1,10 @@ -import type { ActionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { isTheme } from "~/utils/theme-provider"; import { getThemeSession } from "~/utils/theme.server"; -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const themeSession = await getThemeSession(request); const requestText = await request.text(); const form = new URLSearchParams(requestText); diff --git a/dark-mode/app/utils/theme-provider.tsx b/dark-mode/app/utils/theme-provider.tsx index 8edce663..8305713c 100644 --- a/dark-mode/app/utils/theme-provider.tsx +++ b/dark-mode/app/utils/theme-provider.tsx @@ -98,33 +98,34 @@ const clientThemeCode = ` // a theme, then I'll know what you want in the future and you'll not see this // script anymore. ;(() => { - const theme = window.matchMedia(${JSON.stringify(prefersDarkMQ)}).matches - ? 'dark' - : 'light'; + const theme = window.matchMedia('${prefersDarkMQ}').matches + ? '${Theme.DARK}' + : '${Theme.LIGHT}'; + const cl = document.documentElement.classList; - const themeAlreadyApplied = cl.contains('light') || cl.contains('dark'); - if (themeAlreadyApplied) { + if ( + cl.contains('${Theme.LIGHT}') || cl.contains('${Theme.DARK}') + ) { + // The theme is already applied... // this script shouldn't exist if the theme is already applied! - console.warn( - "Hi there, could you let me know you're seeing this message? Thanks!", - ); + console.warn("See theme-provider.tsx>clientThemeCode>cl.contains"); + // Hi there, could you let me know you're seeing this console.warn? Thanks! } else { cl.add(theme); } + const meta = document.querySelector('meta[name=color-scheme]'); if (meta) { - if (theme === 'dark') { - meta.content = 'dark light'; - } else if (theme === 'light') { - meta.content = 'light dark'; - } + meta.content = theme === '${Theme.DARK}' ? 'dark light' : 'light dark'; } else { - console.warn( - "Hey, could you let me know you're seeing this message? Thanks!", - ); + console.warn("See theme-provider.tsx>clientThemeCode>meta"); + // Hey, could you let me know you're seeing this console.warn? Thanks! } })(); -`; +` + // Remove double slash comments & replace excess white space with a single space. + .replace(/((?<=[^:])\/\/.*|\s)+/g, " ") + .trim(); const themeStylesCode = ` /* default light, but app-preference is "dark" */ diff --git a/dataloader/README.md b/dataloader/README.md index e5751b01..e0bf14d4 100644 --- a/dataloader/README.md +++ b/dataloader/README.md @@ -17,7 +17,7 @@ Open this example on [CodeSandbox](https://codesandbox.com): `app/data.server.ts` implements the `db` object which mimics an ORM in the style of [Prisma](https://www.prisma.io/). The method `db.user#findMany` logs _user#findMany_ to the console, for demo purposes. -There's exactly one DataLoader factory `createUsersByIdLoader`, implemented in `app/loaders/userLoader.ts`. It's put on context of [createRequestHandler](https://remix.run/other-api/adapter#createrequesthandler) in `server/index.ts` as `usersById` which is made available to all Remix-loaders and -actions. Both the loaders of `app/routes/users.tsx` and `app/routes/users/_index.tsx` make calls to this loader. When inspecting the server logs while refreshing the page, you'll notice that there's only one log _user#findMany_ per request, proving that with this implementation, there's only one rountrip to the database. +There's exactly one DataLoader factory `createUsersByIdLoader`, implemented in `app/loaders/userLoader.ts`. It's put on context of [createRequestHandler](https://remix.run/other-api/adapter#createrequesthandler) in `server/index.ts` as `usersById` which is made available to all Remix-loaders and -actions. Both the loaders of `app/routes/users.tsx` and `app/routes/users/_index.tsx` make calls to this loader. When inspecting the server logs while refreshing the page, you'll notice that there's only one log _user#findMany_ per request, proving that with this implementation, there's only one roundtrip to the database. ## Related Links diff --git a/google-analytics/.eslintrc.cjs b/google-analytics/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/google-analytics/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/google-analytics/.eslintrc.js b/google-analytics/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/google-analytics/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/google-analytics/.gitignore b/google-analytics/.gitignore index 3f7bf98d..80ec311f 100644 --- a/google-analytics/.gitignore +++ b/google-analytics/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/google-analytics/README.md b/google-analytics/README.md index d9c66bf0..e488bdff 100644 --- a/google-analytics/README.md +++ b/google-analytics/README.md @@ -12,10 +12,14 @@ Open this example on [CodeSandbox](https://codesandbox.com): This example shows how to use Google analytics with Remix. -First you have to get the Google analytics ID and add that key in the [.env.example](./.env.example) file. +- Copy `.env.example` to `.env` +- Configure your Google Analytics ID in the `.env` file (read from the `entry.server.tsx` file) +- Run the project Check [app/root.tsx](./app/root.tsx) where page tracking code is added. For tracking events check [app/routes/contact.tsx](./app/routes/contact.tsx) file. +The Google Analytics tag is disabled in "development", you can enable it during your test, but make sure to only enable tracking in production. + ## Related Links [Google Analytics](https://analytics.google.com/analytics/web/) diff --git a/google-analytics/app/entry.server.tsx b/google-analytics/app/entry.server.tsx new file mode 100644 index 00000000..a60f9802 --- /dev/null +++ b/google-analytics/app/entry.server.tsx @@ -0,0 +1,159 @@ +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { config } from "dotenv"; +import * as isbotModule from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +// Load .env variables +config(); + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + loadContext: AppLoadContext, +) { + const prohibitOutOfOrderStreaming = + isBotRequest(request.headers.get("user-agent")) || remixContext.isSpaMode; + + return prohibitOutOfOrderStreaming + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ); +} + +// We have some Remix apps in the wild already running with isbot@3 so we need +// to maintain backwards compatibility even though we want new apps to use +// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev. +function isBotRequest(userAgent: string | null) { + if (!userAgent) { + return false; + } + + // isbot >= 3.8.0, >4 + if ("isbot" in isbotModule && typeof isbotModule.isbot === "function") { + return isbotModule.isbot(userAgent); + } + + // isbot < 3.8.0 + if ("default" in isbotModule && typeof isbotModule.default === "function") { + return isbotModule.default(userAgent); + } + + return false; +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/google-analytics/app/root.tsx b/google-analytics/app/root.tsx index ca537c74..47989d57 100644 --- a/google-analytics/app/root.tsx +++ b/google-analytics/app/root.tsx @@ -1,10 +1,7 @@ -import type { MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; -// import { json } from "@remix-run/node"; import { Link, Links, - LiveReload, Meta, Outlet, Scripts, @@ -35,13 +32,7 @@ export const loader = async () => { return json({ gaTrackingId: process.env.GA_TRACKING_ID }); }; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { const location = useLocation(); const { gaTrackingId } = useLoaderData(); @@ -54,6 +45,8 @@ export default function App() { return ( + + @@ -81,7 +74,6 @@ export default function App() { /> )} -
- + {children} - ); } + +export default function App() { + return ; +} diff --git a/google-analytics/app/routes/_index.tsx b/google-analytics/app/routes/_index.tsx index b93f4c71..110436ec 100644 --- a/google-analytics/app/routes/_index.tsx +++ b/google-analytics/app/routes/_index.tsx @@ -1,8 +1,10 @@ import type { MetaFunction } from "@remix-run/node"; -export const meta: MetaFunction = () => ({ - title: "Home", -}); +export const meta: MetaFunction = () => [ + { + title: "Home", + }, +]; export default function Index() { return ( diff --git a/google-analytics/app/routes/dashboard.tsx b/google-analytics/app/routes/dashboard.tsx index 5b919567..7bd4058f 100644 --- a/google-analytics/app/routes/dashboard.tsx +++ b/google-analytics/app/routes/dashboard.tsx @@ -1,8 +1,10 @@ import type { MetaFunction } from "@remix-run/node"; -export const meta: MetaFunction = () => ({ - title: "Dashboard", -}); +export const meta: MetaFunction = () => [ + { + title: "Dashboard", + }, +]; export default function Dashboard() { return ( diff --git a/google-analytics/app/routes/profile.tsx b/google-analytics/app/routes/profile.tsx index ca14dcb3..5b23c4c3 100644 --- a/google-analytics/app/routes/profile.tsx +++ b/google-analytics/app/routes/profile.tsx @@ -1,8 +1,10 @@ import type { MetaFunction } from "@remix-run/node"; -export const meta: MetaFunction = () => ({ - title: "Profile", -}); +export const meta: MetaFunction = () => [ + { + title: "Profile", + }, +]; export default function Profile() { return ( diff --git a/google-analytics/package.json b/google-analytics/package.json index a437590d..4ad92a46 100644 --- a/google-analytics/package.json +++ b/google-analytics/package.json @@ -1,29 +1,41 @@ { + "name": "google-analytics", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "dotenv": "^16.4.5", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/google-analytics/remix.config.js b/google-analytics/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/google-analytics/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/google-analytics/remix.env.d.ts b/google-analytics/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/google-analytics/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/google-analytics/tsconfig.json b/google-analytics/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/google-analytics/tsconfig.json +++ b/google-analytics/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/google-analytics/vite.config.ts b/google-analytics/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/google-analytics/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/image-resize/.eslintrc.cjs b/image-resize/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/image-resize/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/image-resize/.eslintrc.js b/image-resize/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/image-resize/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/image-resize/.gitignore b/image-resize/.gitignore index 3f7bf98d..80ec311f 100644 --- a/image-resize/.gitignore +++ b/image-resize/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/image-resize/app/root.tsx b/image-resize/app/root.tsx index 9b0dce37..e82f26fd 100644 --- a/image-resize/app/root.tsx +++ b/image-resize/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "Image Resize Example", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/image-resize/app/routes/assets.resize.$.ts b/image-resize/app/routes/assets.resize.$.ts index e03954fa..715dd515 100644 --- a/image-resize/app/routes/assets.resize.$.ts +++ b/image-resize/app/routes/assets.resize.$.ts @@ -17,7 +17,7 @@ import { createReadStream, statSync } from "fs"; import path from "path"; import { PassThrough } from "stream"; -import type { LoaderArgs } from "@remix-run/node"; +import type { LoaderFunctionArgs } from "@remix-run/node"; import type { Params } from "@remix-run/react"; import type { FitEnum } from "sharp"; import sharp from "sharp"; @@ -31,7 +31,7 @@ interface ResizeParams { fit: keyof FitEnum; } -export const loader = async ({ params, request }: LoaderArgs) => { +export const loader = async ({ params, request }: LoaderFunctionArgs) => { // extract all the parameters from the url const { src, width, height, fit } = extractParams(params, request); diff --git a/image-resize/package.json b/image-resize/package.json index 1c7cd93e..6b377af1 100644 --- a/image-resize/package.json +++ b/image-resize/package.json @@ -1,31 +1,41 @@ { + "name": "image-resize", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "sharp": "^0.30.2" + "sharp": "^0.33.4" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "@types/sharp": "^0.29.5", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } } diff --git a/image-resize/remix.config.js b/image-resize/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/image-resize/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/image-resize/remix.env.d.ts b/image-resize/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/image-resize/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/image-resize/tsconfig.json b/image-resize/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/image-resize/tsconfig.json +++ b/image-resize/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/image-resize/vite.config.ts b/image-resize/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/image-resize/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/infinite-scrolling/app/routes/offset.advanced.tsx b/infinite-scrolling/app/routes/offset.advanced.tsx index 3e13a8a8..6c50a64e 100644 --- a/infinite-scrolling/app/routes/offset.advanced.tsx +++ b/infinite-scrolling/app/routes/offset.advanced.tsx @@ -194,8 +194,8 @@ export default function Index() { {item ? item.value : transition.state === "loading" - ? "Loading more..." - : "Nothing to see here..."} + ? "Loading more..." + : "Nothing to see here..."} ); diff --git a/infinite-scrolling/app/routes/offset.simple.tsx b/infinite-scrolling/app/routes/offset.simple.tsx index 610449d6..402c42a7 100644 --- a/infinite-scrolling/app/routes/offset.simple.tsx +++ b/infinite-scrolling/app/routes/offset.simple.tsx @@ -117,8 +117,8 @@ export default function Index() { {item ? item.value : transition.state === "loading" - ? "Loading more..." - : "Nothing to see here..."} + ? "Loading more..." + : "Nothing to see here..."} ); diff --git a/infinite-scrolling/app/routes/page.advanced.tsx b/infinite-scrolling/app/routes/page.advanced.tsx index 10025965..586d6573 100644 --- a/infinite-scrolling/app/routes/page.advanced.tsx +++ b/infinite-scrolling/app/routes/page.advanced.tsx @@ -197,8 +197,8 @@ export default function Index() { {item ? item.value : transition.state === "loading" - ? "Loading more..." - : "Nothing to see here..."} + ? "Loading more..." + : "Nothing to see here..."} ); diff --git a/infinite-scrolling/app/routes/page.simple.tsx b/infinite-scrolling/app/routes/page.simple.tsx index 70c9587c..98cbcb6c 100644 --- a/infinite-scrolling/app/routes/page.simple.tsx +++ b/infinite-scrolling/app/routes/page.simple.tsx @@ -121,8 +121,8 @@ export default function Index() { {item ? item.value : transition.state === "loading" - ? "Loading more..." - : "Nothing to see here..."} + ? "Loading more..." + : "Nothing to see here..."} ); diff --git a/leaflet/.eslintrc.cjs b/leaflet/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/leaflet/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/leaflet/.eslintrc.js b/leaflet/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/leaflet/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/leaflet/.gitignore b/leaflet/.gitignore index 3f7bf98d..80ec311f 100644 --- a/leaflet/.gitignore +++ b/leaflet/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/leaflet/README.md b/leaflet/README.md index f8f54c15..8534dc01 100644 --- a/leaflet/README.md +++ b/leaflet/README.md @@ -16,21 +16,17 @@ This example shows how to use Leaflet with Remix. Relevant files: -- [app/components/client-only.tsx](app/components/client-only.tsx) - [app/components/map.client.tsx](app/components/map.client.tsx) -Leaflet cannot be rendered on the server side, so we're using the `ClientOnly` component to display a skeleton instead. +Leaflet cannot be rendered on the server side, so we're using the `ClientOnly` component from `remix-utils` to display a skeleton instead. It's important to add the `.client.tsx` suffix to the `Map` component file name, otherwise, you will get this error: ``` Error [ERR_REQUIRE_ESM]: require() of ES Module /remix/examples/leaflet/node_modules/react-leaflet/lib/index.js from /remix/examples/leaflet/build/index.js not supported. ``` -The leaflet styles can be imported either from `node_modules` or CDN. -If you choose to import the styles from `node_modules`, you'll need to add the leaflet assets in the `/public` folder. -The assets can be found here: [https://unpkg.com/browse/leaflet@1.8.0/dist/images/](https://unpkg.com/browse/leaflet@1.8.0/dist/images/) - ## Related Links - [Leaflet docs](https://leafletjs.com/download.html) - [React Leaflet docs](https://react-leaflet.js.org/) +- [remix-utils](https://github.com/sergiodxa/remix-utils) diff --git a/leaflet/app/components/client-only.tsx b/leaflet/app/components/client-only.tsx deleted file mode 100644 index 0446875f..00000000 --- a/leaflet/app/components/client-only.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ReactNode } from "react"; -import { useState, useEffect } from "react"; - -type Props = { - children(): ReactNode; - fallback?: ReactNode; -}; - -let hydrating = true; - -export function ClientOnly({ children, fallback = null }: Props) { - const [hydrated, setHydrated] = useState(() => !hydrating); - - useEffect(function hydrate() { - hydrating = false; - setHydrated(true); - }, []); - - return hydrated ? <>{children()} : <>{fallback}; -} diff --git a/leaflet/app/root.tsx b/leaflet/app/root.tsx index 927a0f74..e82f26fd 100644 --- a/leaflet/app/root.tsx +++ b/leaflet/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/leaflet/app/routes/_index.tsx b/leaflet/app/routes/_index.tsx index 91d8154e..7e69ab87 100644 --- a/leaflet/app/routes/_index.tsx +++ b/leaflet/app/routes/_index.tsx @@ -1,12 +1,13 @@ import type { LinksFunction } from "@remix-run/node"; +import leafletStyles from "leaflet/dist/leaflet.css?url"; +import { ClientOnly } from "remix-utils/client-only"; -import { ClientOnly } from "~/components/client-only"; import { Map } from "~/components/map.client"; export const links: LinksFunction = () => [ { rel: "stylesheet", - href: "https://unpkg.com/leaflet@1.8.0/dist/leaflet.css", + href: leafletStyles, }, ]; diff --git a/leaflet/package.json b/leaflet/package.json index 0c19d3b0..a9b519ca 100644 --- a/leaflet/package.json +++ b/leaflet/package.json @@ -1,32 +1,44 @@ { + "name": "leaflet", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "leaflet": "^1.8.0", - "react-leaflet": "^4.0.2", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "leaflet": "^1.9.4", + "isbot": "^4.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", + "remix-utils": "^7.6.0" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/leaflet": "^1.7.11", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@remix-run/dev": "^2.9.2", + "@types/leaflet": "^1.9.12", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/leaflet/remix.config.js b/leaflet/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/leaflet/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/leaflet/remix.env.d.ts b/leaflet/remix.env.d.ts deleted file mode 100644 index 72e2affe..00000000 --- a/leaflet/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/leaflet/tsconfig.json b/leaflet/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/leaflet/tsconfig.json +++ b/leaflet/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/leaflet/vite.config.ts b/leaflet/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/leaflet/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/msw/.eslintrc.cjs b/msw/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/msw/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/msw/.eslintrc.js b/msw/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/msw/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/msw/.gitignore b/msw/.gitignore index 3f7bf98d..80ec311f 100644 --- a/msw/.gitignore +++ b/msw/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/msw/README.md b/msw/README.md index e9b48168..ea9d32e9 100644 --- a/msw/README.md +++ b/msw/README.md @@ -19,15 +19,10 @@ We can mock the HTTP calls using MSW, which intercepts the API calls from the se You can read more about the use cases of MSW [here](https://mswjs.io/docs/#when-to-mock-api) -## Gotchas - -MSW currently does not support intercepting requests made by [undici](https://undici.nodejs.org/#/). For local development, Cloudflare Workers and Pages simulates the production environment using [wrangler](https://developers.cloudflare.com/workers/cli-wrangler), which runs [miniflare](https://github.com/cloudflare/miniflare) internally. `Miniflare` implements `fetch` using `undici` instead of `node-fetch`. You can follow this issue [#159](https://github.com/mswjs/interceptors/issues/159) to track the progress. - ## Relevant files -- [mocks](./mocks/index.js) - registers the Node HTTP mock server -- [handlers](./mocks/handlers.js) - describes the HTTP mocks -- [root](./app/root.tsx) +- [mocks](./mocks/index.cjs) - registers the Node HTTP mock server +- [handlers](./mocks/handlers.cjs) - describes the HTTP mocks - [package.json](./package.json) ## Related Links diff --git a/msw/app/root.tsx b/msw/app/root.tsx index 927a0f74..e82f26fd 100644 --- a/msw/app/root.tsx +++ b/msw/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/msw/mocks/handlers.cjs b/msw/mocks/handlers.cjs new file mode 100644 index 00000000..67e6918c --- /dev/null +++ b/msw/mocks/handlers.cjs @@ -0,0 +1,9 @@ +const { http, HttpResponse } = require("msw"); + +const handlers = [ + http.get("https://my-mock-api.com", () => { + return HttpResponse.json({ message: "from msw" }); + }), +]; + +module.exports = handlers; diff --git a/msw/mocks/handlers.js b/msw/mocks/handlers.js deleted file mode 100644 index f95cce06..00000000 --- a/msw/mocks/handlers.js +++ /dev/null @@ -1,9 +0,0 @@ -const { rest } = require("msw"); - -const handlers = [ - rest.get("https://my-mock-api.com", (_, res, ctx) => { - return res(ctx.status(200), ctx.json({ message: "from msw" })); - }), -]; - -module.exports = handlers; diff --git a/msw/mocks/index.js b/msw/mocks/index.cjs similarity index 63% rename from msw/mocks/index.js rename to msw/mocks/index.cjs index 6b3dd801..95b22594 100644 --- a/msw/mocks/index.js +++ b/msw/mocks/index.cjs @@ -1,7 +1,7 @@ const { setupServer } = require("msw/node"); -const handlers = require("./handlers"); +const handlers = require("./handlers.cjs"); const server = setupServer(...handlers); server.listen({ onUnhandledRequest: "warn" }); -console.info("MSW initialised"); +console.info("MSW initialized"); diff --git a/msw/package.json b/msw/package.json index 813b0f88..0c691a6b 100644 --- a/msw/package.json +++ b/msw/package.json @@ -1,31 +1,42 @@ { + "name": "msw", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "binode --require ./mocks -- @remix-run/dev:remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "binode --require ./mocks/index.cjs -- @remix-run/dev:remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", "binode": "^1.0.5", - "eslint": "^8.27.0", - "msw": "^0.47.3", - "typescript": "^4.8.4" + "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", + "msw": "^2.3", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } } diff --git a/msw/remix.config.js b/msw/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/msw/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/msw/remix.env.d.ts b/msw/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/msw/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/msw/tsconfig.json b/msw/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/msw/tsconfig.json +++ b/msw/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/msw/vite.config.ts b/msw/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/msw/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/multiple-forms/app/data.server.ts b/multiple-forms/app/data.server.ts index 189641f2..4b1d9c0c 100644 --- a/multiple-forms/app/data.server.ts +++ b/multiple-forms/app/data.server.ts @@ -20,7 +20,7 @@ async function writeInvitations(invitations: Array) { return fs.writeFile("./data.json", JSON.stringify({ invitations }, null, 2)); } -export async function deleteInvitiation(invitation: Invitation) { +export async function deleteInvitation(invitation: Invitation) { const invitations = await getInvitations(); await writeInvitations(invitations.filter((i) => i.id !== invitation.id)); } diff --git a/multiple-forms/app/routes/invitations.tsx b/multiple-forms/app/routes/invitations.tsx index 0c2333e0..fc9721aa 100644 --- a/multiple-forms/app/routes/invitations.tsx +++ b/multiple-forms/app/routes/invitations.tsx @@ -3,6 +3,7 @@ import { json, redirect } from "@remix-run/node"; import { Form, useLoaderData } from "@remix-run/react"; import { + deleteInvitation, getInvitations, resendInvitation, sendInvitation, @@ -30,8 +31,8 @@ export const action = async ({ request }: ActionArgs) => { // you'll want to handle this in a real app... throw new Error("make sure you implement validation"); } - const invitiations = await getInvitations(); - const invitation = invitiations.find((i) => i.id === invitationId); + const invitations = await getInvitations(); + const invitation = invitations.find((i) => i.id === invitationId); if (!invitation) { // you'll want to handle this in a real app... throw new Error("make sure you implement validation"); @@ -42,7 +43,7 @@ export const action = async ({ request }: ActionArgs) => { return redirect(request.url); } if (formData.get("intent") === "delete") { - await deleteInvitiation(invitation); + await deleteInvitation(invitation); return redirect(request.url); } }; diff --git a/mux-video/.env.example b/mux-video/.env.example new file mode 100644 index 00000000..c3941d41 --- /dev/null +++ b/mux-video/.env.example @@ -0,0 +1,2 @@ +MUX_TOKEN_ID= +MUX_TOKEN_SECRET= diff --git a/mux-video/.eslintrc.cjs b/mux-video/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/mux-video/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/usematches-loader-data/.gitignore b/mux-video/.gitignore similarity index 70% rename from usematches-loader-data/.gitignore rename to mux-video/.gitignore index 3f7bf98d..80ec311f 100644 --- a/usematches-loader-data/.gitignore +++ b/mux-video/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/mux-video/README.md b/mux-video/README.md new file mode 100644 index 00000000..0bbbfdbd --- /dev/null +++ b/mux-video/README.md @@ -0,0 +1,71 @@ +# Mux Video + +This example uses Mux Video, an API-first platform for video. The example features video uploading and playback in a Remix.js application. + +This example is useful if you want to build a platform that supports user-uploaded videos. For example: + +- Enabling user profile videos +- Accepting videos for a video contest promotion +- Allowing customers to upload screencasts that help with troubleshooting a bug +- Or even the next Youtube, TikTok, or Instagram + +## Preview + +Open this example on [CodeSandbox](https://codesandbox.com): + +[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/mux-video) + +## How to use + +### Step 1. Create a Remix app with this example + +```bash +npx create-remix@latest --template remix-run/examples/mux-video +``` + +### Step 2. Create an account in Mux + +All you need to run this example is a [Mux account](https://www.mux.com?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples). + +Before entering a credit card on your Mux account, all videos are in “test mode” which means they are watermarked and clipped to 10 seconds. + +### Step 3. Set up environment variables + +Copy the `.env.example` file in this directory to `.env` (which will be ignored by Git): + +```bash +cp .env.example .env +``` + +Then, go to the [settings page](https://dashboard.mux.com/settings/access-tokens) in your Mux dashboard, get a new **API Access Token** with "Mux Video Read" and "Mux Video Write" permissions. Use that token to set the variables in `.env.local`: + +- `MUX_TOKEN_ID` should be the `TOKEN ID` of your new token +- `MUX_TOKEN_SECRET` should be `TOKEN SECRET` + +At this point, you're good to `npm run dev`. + +## How it works + +Uploading and viewing a video takes four steps: + +1. **Upload a video**: Use the Mux [Direct Uploads API](https://docs.mux.com/api-reference#video/tag/direct-uploads?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples) to create an endpoint for [Mux Uploader React](https://docs.mux.com/guides/mux-uploader?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples). The user can then use Mux Uploader to upload a video. +1. **Exchange the `upload.id` for an `asset.id`**: Once the upload is complete, it will have a Mux asset associated with it. We can use the [Direct Uploads API](https://docs.mux.com/api-reference#video/tag/direct-uploads?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples) to check for that asset. +1. **Use the `asset.id` to check if the asset is ready** by polling the [Asset API](https://docs.mux.com/api-reference#video/tag/assets?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples) +1. **Play back the video with [Mux Player React](https://docs.mux.com/guides/mux-player-web?utm_source=remix-examples&utm_medium=mux-video&utm_campaign=remix-examples)** (on a page that uses the [Mux Image API](https://docs.mux.com/guides/get-images-from-a-video) to provide og images) + +These steps correspond to the following routes: + +1. [`_index.tsx`](app/routes/_index.tsx) creates the upload in a loader, and exchanges the `upload.id` for an `asset.id` in an action which redirects to... +2. [`status.$assetId.tsx`](app/routes/status.$assetId.tsx) polls the Mux API to see if the asset is ready. When it is, we redirect to... +3. [`playback.$playbackId.tsx`](app/routes/playback.$playbackId.tsx) plays the video. + +## Preparing for Production + +### Set the cors_origin + +When creating uploads, this demo sets `cors_origin: "*"` in the [`app/routes/_index.tsx`](app/routes/_index.tsx) file. For extra security, you should update this value to be something like `cors_origin: 'https://your-app.com'`, to restrict uploads to only be allowed from your application. + +### Consider webhooks + +In this example, we poll the Mux API to see if our asset is ready. In production, you'll likely have a database where you can store the `upload.id` and `asset.id`, and you can use [Mux Webhooks](https://docs.mux.com/guides/listen-for-webhooks) to get notified when your upload is complete, and when your asset is ready. +In this example, we poll the Mux API to see if our asset is ready. In production, you'll likely have a database where you can store the `upload.id` and `asset.id`, and you can use [Mux Webhooks](https://docs.mux.com/guides/listen-for-webhooks) to get notified when your upload is complete, and when your asset is ready. diff --git a/mux-video/app/lib/mux.server.ts b/mux-video/app/lib/mux.server.ts new file mode 100644 index 00000000..67344c5b --- /dev/null +++ b/mux-video/app/lib/mux.server.ts @@ -0,0 +1,5 @@ +import Mux from "@mux/mux-node"; + +const mux = new Mux(); + +export default mux; diff --git a/mux-video/app/root.tsx b/mux-video/app/root.tsx new file mode 100644 index 00000000..e82f26fd --- /dev/null +++ b/mux-video/app/root.tsx @@ -0,0 +1,29 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/mux-video/app/routes/_index.tsx b/mux-video/app/routes/_index.tsx new file mode 100644 index 00000000..8c05554b --- /dev/null +++ b/mux-video/app/routes/_index.tsx @@ -0,0 +1,69 @@ +import MuxUploader from "@mux/mux-uploader-react"; +import { type ActionFunctionArgs, json, redirect } from "@remix-run/node"; +import { Form, useActionData, useLoaderData } from "@remix-run/react"; +import { useState } from "react"; + +import mux from "~/lib/mux.server"; + +export const loader = async () => { + // Create an endpoint for MuxUploader to upload to + const upload = await mux.video.uploads.create({ + new_asset_settings: { + playback_policy: ["public"], + encoding_tier: "baseline", + }, + // in production, you'll want to change this origin to your-domain.com + cors_origin: "*", + }); + return json({ id: upload.id, url: upload.url }); +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + const formData = await request.formData(); + const uploadId = formData.get("uploadId"); + if (typeof uploadId !== "string") { + throw new Error("No uploadId found"); + } + + // when the upload is complete, + // the upload will have an assetId associated with it + // we'll use that assetId to view the video status + const upload = await mux.video.uploads.retrieve(uploadId); + if (upload.asset_id) { + return redirect(`/status/${upload.asset_id}`); + } + + // while onSuccess is a strong indicator that Mux has received the file + // and created the asset, this isn't a guarantee. + // In production, you might write an api route + // to listen for the`video.upload.asset_created` webhook + // https://docs.mux.com/guides/listen-for-webhooks + // However, to keep things simple here, + // we'll just ask the user to push the button again. + // This should rarely happen. + return json({ message: "Upload has no asset yet. Try again." }); +}; + +export default function UploadPage() { + const loaderData = useLoaderData(); + const actionData = useActionData(); + const [isUploadSuccess, setIsUploadSuccess] = useState(false); + + const { id, url } = loaderData; + const { message } = actionData ?? {}; + + return ( +
+ setIsUploadSuccess(true)} /> + + {/* + you might have other fields here, like name and description, + that you'll save in your CMS alongside the uploadId and assetId + */} + + {message ?

{message}

: null} + + ); +} diff --git a/mux-video/app/routes/mux.webhook.ts b/mux-video/app/routes/mux.webhook.ts new file mode 100644 index 00000000..958f96b5 --- /dev/null +++ b/mux-video/app/routes/mux.webhook.ts @@ -0,0 +1,43 @@ +import { json, type ActionFunctionArgs } from "@remix-run/node"; + +import mux from "~/lib/mux.server"; + +// while this isn't called anywhere in this example, +// I thought it might be helpful to see what a mux webhook handler looks like. + +// Mux webhooks POST, so let's use an action +export const action = async ({ request }: ActionFunctionArgs) => { + if (request.method !== "POST") { + return new Response("Method not allowed", { status: 405 }); + } + + const body = await request.text(); + // mux.webhooks.unwrap will validate that the given payload was sent by Mux and parse the payload. + // It will also provide type-safe access to the payload. + // Generate MUX_WEBHOOK_SIGNING_SECRET in the Mux dashboard + // https://dashboard.mux.com/settings/webhooks + const event = mux.webhooks.unwrap( + body, + request.headers, + process.env.MUX_WEBHOOK_SIGNING_SECRET, + ); + + // you can also unwrap the payload yourself: + // const event = await request.json(); + switch (event.type) { + case "video.upload.asset_created": + // we might use this to know that an upload has been completed + // and we can save its assetId to our database + break; + case "video.asset.ready": + // we might use this to know that a video has been encoded + // and we can save its playbackId to our database + break; + // there are many more Mux webhook events + // check them out at https://docs.mux.com/webhook-reference + default: + break; + } + + return json({ message: "ok" }); +}; diff --git a/mux-video/app/routes/playback.$playbackId.tsx b/mux-video/app/routes/playback.$playbackId.tsx new file mode 100644 index 00000000..17723ce2 --- /dev/null +++ b/mux-video/app/routes/playback.$playbackId.tsx @@ -0,0 +1,57 @@ +import MuxPlayer from "@mux/mux-player-react"; +import type { MetaFunction } from "@remix-run/node"; +import { useParams, Link } from "@remix-run/react"; + +const title = "View this video created with Mux + Remix"; +const description = + "This video was uploaded and processed by Mux in an example Remix application."; +export const meta: MetaFunction = ({ params }) => { + const { playbackId } = params; + return [ + { name: "description", content: description }, + { property: "og:type", content: "video" }, + { property: "og:title", content: title }, + { property: "og:description", content: description }, + { + property: "og:image", + content: `https://image.mux.com/${playbackId}/thumbnail.png?width=1200&height=630&fit_mode=pad`, + }, + { property: "og:image:width", content: "1200" }, + { property: "og:image:height", content: "630" }, + { property: "twitter:card", content: "summary_large_image" }, + { property: "twitter:title", content: title }, + { property: "twitter:description", content: description }, + { + property: "twitter:image", + content: `https://image.mux.com/${playbackId}/thumbnail.png?width=1200&height=600&fit_mode=pad`, + }, + { property: "twitter:image:width", content: "1200" }, + { property: "twitter:image:height", content: "600" }, + // These tags should be sufficient for social sharing. + // However, if you're really committed video SEO, I'd suggest adding ld+json, as well. + // https://developers.google.com/search/docs/appearance/structured-data/video + ]; +}; + +export default function Page() { + const { playbackId } = useParams(); + return ( + <> +

This video is ready for playback and sharing

+ +

+ Go back home to upload another video. +

+ + ); +} diff --git a/mux-video/app/routes/status.$assetId.tsx b/mux-video/app/routes/status.$assetId.tsx new file mode 100644 index 00000000..368078e4 --- /dev/null +++ b/mux-video/app/routes/status.$assetId.tsx @@ -0,0 +1,79 @@ +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; +import { useFetcher, useLoaderData, useParams, Link } from "@remix-run/react"; +import { useEffect } from "react"; + +import mux from "~/lib/mux.server"; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + // now that we have an assetId, we can see how the video is doing. + // in production, you might have some setup where a Mux webhook + // tells your server the status of your asset. + // https://docs.mux.com/guides/listen-for-webhooks + // for this example, however, we'll just ask the Mux API ourselves + const { assetId } = params; + if (typeof assetId !== "string") { + throw new Error("No assetId found"); + } + const asset = await mux.video.assets.retrieve(assetId); + + // if the asset is ready and it has a public playback ID, + // (which it should, considering the upload settings we used) + // redirect to its playback page + if (asset.status === "ready") { + const playbackIds = asset.playback_ids; + if (Array.isArray(playbackIds)) { + const playbackId = playbackIds.find((id) => id.policy === "public"); + if (playbackId) { + return redirect(`/playback/${playbackId.id}`); + } + } + } + + // if the asset is not ready, we'll keep polling + return json({ + status: asset.status, + errors: asset.errors, + }); +}; + +export default function UploadStatus() { + const { status, errors } = useLoaderData(); + const { assetId } = useParams(); + const fetcher = useFetcher(); + + // we'll poll the API by running the loader every two seconds + useEffect(() => { + if (status !== "preparing") return; + + const interval = setInterval(() => { + fetcher.load(`/status/${assetId}`); + }, 2000); + return () => clearInterval(interval); + }, [fetcher, assetId, status]); + + if (status === "preparing") { + return

Asset is preparing...

; + } + + // if not preparing, then "errored" or "ready" + // if "errored", we'll show the errors + // we don't expect to see "ready" because "ready" should redirect in the action + return ( + <> +

+ Asset is in an unexpected state: {status}. +

+ {Array.isArray(errors) ? ( +
    + {errors.map((error, key) => ( +
  • {JSON.stringify(error)}
  • + ))} +
+ ) : null} +

+ This is awkward. Let's refresh and try again. +

+ + ); +} diff --git a/mux-video/package.json b/mux-video/package.json new file mode 100644 index 00000000..dbea06e9 --- /dev/null +++ b/mux-video/package.json @@ -0,0 +1,43 @@ +{ + "name": "template", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", + "typecheck": "tsc" + }, + "dependencies": { + "@mux/mux-node": "^8.7.0", + "@mux/mux-player-react": "^2.7.0", + "@mux/mux-uploader-react": "^1.0.0-beta.18", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/usematches-loader-data/public/favicon.ico b/mux-video/public/favicon.ico similarity index 100% rename from usematches-loader-data/public/favicon.ico rename to mux-video/public/favicon.ico diff --git a/usematches-loader-data/sandbox.config.json b/mux-video/sandbox.config.json similarity index 100% rename from usematches-loader-data/sandbox.config.json rename to mux-video/sandbox.config.json diff --git a/mux-video/tsconfig.json b/mux-video/tsconfig.json new file mode 100644 index 00000000..9d87dd37 --- /dev/null +++ b/mux-video/tsconfig.json @@ -0,0 +1,32 @@ +{ + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true + } +} diff --git a/mux-video/vite.config.ts b/mux-video/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/mux-video/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/newsletter-signup/app/routes/newsletter.tsx b/newsletter-signup/app/routes/newsletter.tsx index 1cd0beab..56cb1c20 100644 --- a/newsletter-signup/app/routes/newsletter.tsx +++ b/newsletter-signup/app/routes/newsletter.tsx @@ -30,10 +30,10 @@ export default function Newsletter() { transition.submission ? "submitting" : actionData?.subscription - ? "success" - : actionData?.error - ? "error" - : "idle"; + ? "success" + : actionData?.error + ? "error" + : "idle"; const inputRef = useRef(null); const successRef = useRef(null); diff --git a/nprogress/.eslintrc.cjs b/nprogress/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/nprogress/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/nprogress/.eslintrc.js b/nprogress/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/nprogress/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/nprogress/.gitignore b/nprogress/.gitignore index 3f7bf98d..80ec311f 100644 --- a/nprogress/.gitignore +++ b/nprogress/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/nprogress/app/root.tsx b/nprogress/app/root.tsx index e9bf9349..d7883914 100644 --- a/nprogress/app/root.tsx +++ b/nprogress/app/root.tsx @@ -1,15 +1,14 @@ import type { LinksFunction, MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, - useTransition, + useNavigation, } from "@remix-run/react"; import NProgress from "nprogress"; -import nProgressStyles from "nprogress/nprogress.css"; +import nProgressStyles from "nprogress/nprogress.css?url"; import { useEffect } from "react"; export const links: LinksFunction = () => [ @@ -17,34 +16,35 @@ export const links: LinksFunction = () => [ { rel: "stylesheet", href: nProgressStyles }, ]; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { - const transition = useTransition(); - useEffect(() => { - // when the state is idle then we can to complete the progress bar - if (transition.state === "idle") NProgress.done(); - // and when it's something else it means it's either submitting a form or - // waiting for the loaders of the next location so we start it - else NProgress.start(); - }, [transition.state]); +export const meta: MetaFunction = () => [{ title: "New Remix App" }]; +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + const navigation = useNavigation(); + useEffect(() => { + // when the state is idle then we can to complete the progress bar + if (navigation.state === "idle") NProgress.done(); + // and when it's something else it means it's either submitting a form or + // waiting for the loaders of the next location so we start it + else NProgress.start(); + }, [navigation.state]); + + return ; +} diff --git a/nprogress/package.json b/nprogress/package.json index 3fa146a1..47dc8a7b 100644 --- a/nprogress/package.json +++ b/nprogress/package.json @@ -1,31 +1,42 @@ { + "name": "nprogress2", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "nprogress": "^0.2.0", - "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", + "@remix-run/dev": "^2.9.2", "@types/nprogress": "^0.2.0", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } } diff --git a/nprogress/remix.config.js b/nprogress/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/nprogress/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/nprogress/remix.env.d.ts b/nprogress/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/nprogress/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/nprogress/tsconfig.json b/nprogress/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/nprogress/tsconfig.json +++ b/nprogress/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/nprogress/vite.config.ts b/nprogress/vite.config.ts new file mode 100644 index 00000000..2b6aff98 --- /dev/null +++ b/nprogress/vite.config.ts @@ -0,0 +1,10 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { installGlobals } from "@remix-run/node"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +installGlobals(); + +export default defineConfig({ + plugins: [remix(), tsconfigPaths()], +}); diff --git a/package.json b/package.json index de3cd4e6..4be7db0a 100644 --- a/package.json +++ b/package.json @@ -4,23 +4,23 @@ "license": "MIT", "scripts": { "format": "prettier --write . && yarn lint:fix", - "lint": "eslint .", + "lint": "eslint --no-eslintrc --config .eslintrc.js .", "lint:fix": "yarn lint --fix" }, "devDependencies": { - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.2.30", - "eslint": "^8.51.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-markdown": "^3.0.1", - "eslint-plugin-cypress": "^2.15.1", + "@remix-run/eslint-config": "^2.9.2", + "@types/react": "^18.3.3", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-markdown": "^5.0.0", + "eslint-plugin-cypress": "^3.3.0", "eslint-plugin-prefer-let": "^3.0.1", "jest": "^29.7.0", - "prettier": "^3.0.3", - "react": "^18.2.0", - "typescript": "^5.2.2" + "prettier": "^3.2.5", + "react": "^18.3.1", + "typescript": "^5.4.5" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/picocss/app/routes/examples.tsx b/picocss/app/routes/examples.tsx index 0805c483..64c908c5 100644 --- a/picocss/app/routes/examples.tsx +++ b/picocss/app/routes/examples.tsx @@ -92,7 +92,7 @@ export default function Examples() {
@@ -125,15 +125,15 @@ export default function Examples() {

Inline text elements

- Primary link + Primary link

- + Secondary link

- + Contrast link

diff --git a/playwright/.eslintrc.cjs b/playwright/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/playwright/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/playwright/.eslintrc.js b/playwright/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/playwright/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/playwright/.gitignore b/playwright/.gitignore index aef542f6..3657c73b 100644 --- a/playwright/.gitignore +++ b/playwright/.gitignore @@ -2,8 +2,7 @@ node_modules /.cache /build -/public/build .env test-results -playwright-report +playwright-report \ No newline at end of file diff --git a/playwright/README.md b/playwright/README.md index f11d3bd4..286ec9c7 100644 --- a/playwright/README.md +++ b/playwright/README.md @@ -11,6 +11,7 @@ Open this example on [CodeSandbox](https://codesandbox.com): ## Example This example shows how to use Playwright to write e2e tests with Remix. +Install dependencies and run the `test:e2e` script. ## Relevant files diff --git a/playwright/app/root.tsx b/playwright/app/root.tsx index 927a0f74..e82f26fd 100644 --- a/playwright/app/root.tsx +++ b/playwright/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/playwright/package.json b/playwright/package.json index 391bd180..1dfec9ba 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -1,31 +1,42 @@ { + "name": "playwright", "private": true, "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "test:e2e": "playwright test", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@playwright/test": "^1.27.1", - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", - "eslint": "^8.27.0", - "typescript": "^4.8.4" + "@playwright/test": "^1.44.1", + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } } diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index 7ab9ad1b..0b36534c 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -1,7 +1,7 @@ import type { PlaywrightTestConfig } from "@playwright/test"; import { devices } from "@playwright/test"; -const PORT = process.env.PORT || 3000; +const PORT = process.env.PORT || 5173; const baseURL = `http://localhost:${PORT}`; diff --git a/playwright/remix.config.js b/playwright/remix.config.js deleted file mode 100644 index ca00ba94..00000000 --- a/playwright/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/playwright/remix.env.d.ts b/playwright/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/playwright/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/playwright/tsconfig.json b/playwright/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/playwright/tsconfig.json +++ b/playwright/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/playwright/vite.config.ts b/playwright/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/playwright/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/pm-app/app/ui/flex.tsx b/pm-app/app/ui/flex.tsx index 5e72b161..2dd5edd8 100644 --- a/pm-app/app/ui/flex.tsx +++ b/pm-app/app/ui/flex.tsx @@ -32,10 +32,10 @@ const Flex = React.forwardRef( wrap === true ? "wrap" : wrap === false || wrap === "nowrap" - ? "nowrap" - : wrap === "reverse" - ? "wrap-reverse" - : wrap + ? "nowrap" + : wrap === "reverse" + ? "wrap-reverse" + : wrap }`]: wrap != null, [`${COMP_CLASS}--align-items-${alignItems}`]: alignItems != null, [`${COMP_CLASS}--align-content-${alignContent}`]: diff --git a/pm-app/app/utils/types.ts b/pm-app/app/utils/types.ts index 6993dae4..7c69bc28 100644 --- a/pm-app/app/utils/types.ts +++ b/pm-app/app/utils/types.ts @@ -29,6 +29,6 @@ export type RemoveIndex = { [K in keyof T as string extends K ? never : number extends K - ? never - : K]: T[K]; + ? never + : K]: T[K]; }; diff --git a/remix-auth-form/.eslintrc.cjs b/remix-auth-form/.eslintrc.cjs index 2061cd22..4f6f59ee 100644 --- a/remix-auth-form/.eslintrc.cjs +++ b/remix-auth-form/.eslintrc.cjs @@ -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, + }, + }, + ], }; diff --git a/remix-auth-form/.gitignore b/remix-auth-form/.gitignore index 3f7bf98d..80ec311f 100644 --- a/remix-auth-form/.gitignore +++ b/remix-auth-form/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/remix-auth-form/README.md b/remix-auth-form/README.md index 848e0581..902a734e 100644 --- a/remix-auth-form/README.md +++ b/remix-auth-form/README.md @@ -1,22 +1,20 @@ -# Remix Auth - FormStrategy +# TODO: Title of Example -Authentication using Remix Auth with the FormStrategy. +TODO: Describe the use case here ## Preview Open this example on [CodeSandbox](https://codesandbox.com): -[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/remix-auth-form) + -## Example - -This is using Remix Auth and the `remix-auth-form` packages. +[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/__template) -The `/login` route renders a form with a email and password input. After a submit it runs some validations and store the user email in the session. +## Example -The `/private` routes redirects the user to `/login` if it's not logged-in, or shows the user email and a logout form if it's logged-in. +Describe the example and how it demonstrates solving the problem. Reference any relevant files/dependencies if needed. +If your example needs to modify the `entry.client.tsx` or `entry.server.tsx` files, use `npx remix reveal` to reveal them. ## Related Links -- [Remix Auth](https://github.com/sergiodxa/remix-auth) -- [Remix Auth Form](https://github.com/sergiodxa/remix-auth-form) +Link to documentation or other related examples. diff --git a/remix-auth-form/app/root.tsx b/remix-auth-form/app/root.tsx index f7bdf668..e82f26fd 100644 --- a/remix-auth-form/app/root.tsx +++ b/remix-auth-form/app/root.tsx @@ -1,32 +1,29 @@ -import type { MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -export const meta: MetaFunction = () => ([{ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}]); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/remix-auth-form/app/routes/_index.tsx b/remix-auth-form/app/routes/_index.tsx index bf18529f..1e098ad6 100644 --- a/remix-auth-form/app/routes/_index.tsx +++ b/remix-auth-form/app/routes/_index.tsx @@ -2,10 +2,9 @@ import { Link } from "@remix-run/react"; export default function Index() { return ( - <> -
/
-

Welcome

+
+

Welcome to Remix

Login - - ) -} \ No newline at end of file +
+ ); +} diff --git a/remix-auth-form/package.json b/remix-auth-form/package.json index 3464401c..f1232a83 100644 --- a/remix-auth-form/package.json +++ b/remix-auth-form/package.json @@ -1,33 +1,42 @@ { + "name": "template", "private": true, - "type": "module", "sideEffects": false, + "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev --manual", - "start": "remix-serve build", + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", "typecheck": "tsc" }, "dependencies": { - "@remix-run/node": "^2.1.0", - "@remix-run/react": "^2.1.0", - "@remix-run/serve": "^2.1.0", - "@remix-run/server-runtime": "2.1.0", - "isbot": "^3.6.5", + "@remix-run/node": "^2.9.2", + "@remix-run/react": "^2.9.2", + "@remix-run/serve": "^2.9.2", + "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "remix-auth": "^3.6.0", - "remix-auth-form": "^1.4.0" + "remix-auth": "^3.7.0", + "remix-auth-form": "^1.5.0" }, "devDependencies": { - "@remix-run/dev": "^2.1.0", - "@remix-run/eslint-config": "^2.1.0", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", + "@remix-run/dev": "^2.9.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", "eslint": "^8.38.0", - "typescript": "^5.1.6" + "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", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } -} \ No newline at end of file +} diff --git a/remix-auth-form/remix.config.js b/remix-auth-form/remix.config.js deleted file mode 100644 index 7fac2d30..00000000 --- a/remix-auth-form/remix.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('@remix-run/dev').AppConfig} */ -export default { - ignoredRouteFiles: ["**/.*"], - // appDirectory: "app", - // assetsBuildDirectory: "public/build", - // publicPath: "/build/", - // serverBuildPath: "build/index.js", -}; diff --git a/remix-auth-form/remix.env.d.ts b/remix-auth-form/remix.env.d.ts deleted file mode 100644 index dcf8c45e..00000000 --- a/remix-auth-form/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/remix-auth-form/tsconfig.json b/remix-auth-form/tsconfig.json index 20f8a386..9d87dd37 100644 --- a/remix-auth-form/tsconfig.json +++ b/remix-auth-form/tsconfig.json @@ -1,22 +1,32 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.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`. + // Vite takes care of building everything, not tsc. "noEmit": true } } diff --git a/remix-auth-form/vite.config.ts b/remix-auth-form/vite.config.ts new file mode 100644 index 00000000..54066fb7 --- /dev/null +++ b/remix-auth-form/vite.config.ts @@ -0,0 +1,16 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], +}); diff --git a/rust/.eslintrc.cjs b/rust/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/rust/.eslintrc.cjs @@ -0,0 +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 = { + 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, + }, + }, + ], +}; diff --git a/rust/.eslintrc.js b/rust/.eslintrc.js deleted file mode 100644 index 2061cd22..00000000 --- a/rust/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], -}; diff --git a/rust/.gitignore b/rust/.gitignore index 3f7bf98d..80ec311f 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -2,5 +2,4 @@ node_modules /.cache /build -/public/build .env diff --git a/rust/README.md b/rust/README.md index f1b2178a..3ef7e8ec 100644 --- a/rust/README.md +++ b/rust/README.md @@ -20,7 +20,7 @@ Installing Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -Here we use `wasm-pack` to comile our Rust code down to WASM +Here we use `wasm-pack` to compile our Rust code down to WASM Installing [wasm-pack](https://github.com/rustwasm/wasm-pack): @@ -41,7 +41,7 @@ cd wasm-pack build --target nodejs ``` -After succesfully bulding the library you can add this to your dependencies by running inside your remix project: +After successfully building the library you can add this to your dependencies by running inside your remix project: ```sh npm install .//pkg @@ -51,7 +51,7 @@ This will add the dependency to your `package.json` file. ```json ... - "@remix-run/serve": "1.1.3", + "@remix-run/serve": "2.9.2", "": "file:/pkg" ... ``` diff --git a/rust/app/root.tsx b/rust/app/root.tsx index 1836c5e3..dc5dec4c 100644 --- a/rust/app/root.tsx +++ b/rust/app/root.tsx @@ -1,38 +1,35 @@ -import type { LinksFunction, MetaFunction } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -import globalStylesUrl from "~/styles/global.css"; +import globalStylesUrl from "~/styles/global.css?url"; export const links: LinksFunction = () => [ { rel: "stylesheet", href: globalStylesUrl }, ]; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - title: "New Remix App", - viewport: "width=device-width,initial-scale=1", -}); - -export default function App() { +export function Layout({ children }: { children: React.ReactNode }) { return ( + + - + {children} - ); } + +export default function App() { + return ; +} diff --git a/rust/app/routes/_index.tsx b/rust/app/routes/_index.tsx index 0b3f4f2c..d2de66e5 100644 --- a/rust/app/routes/_index.tsx +++ b/rust/app/routes/_index.tsx @@ -1,15 +1,15 @@ -import type { ActionArgs, LinksFunction } from "@remix-run/node"; +import type { ActionFunctionArgs, LinksFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; import { add } from "~/rust.server"; -import indexStylesUrl from "~/styles/index.css"; +import indexStylesUrl from "~/styles/index.css?url"; export const links: LinksFunction = () => [ { rel: "stylesheet", href: indexStylesUrl }, ]; -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const { left_operand, operator, right_operand } = Object.fromEntries(formData); @@ -41,6 +41,7 @@ export default function Index() { name="left_operand" id="left_operand" placeholder="2" + required />