diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c83b1a5814..15536334ab 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,10 +8,10 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
- version: 7
+ version: 8
- uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 18
cache: pnpm
- run: make lint
- run: pnpm i
@@ -25,10 +25,10 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
- version: 7
+ version: 8
- uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 18
cache: pnpm
- run: pnpm i
- run: make docker/up db/reset/test db/seed-download
@@ -44,10 +44,10 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
- version: 7
+ version: 8
- uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 18
cache: pnpm
- run: pnpm i
- uses: actions/cache@v3
@@ -76,10 +76,10 @@ jobs:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
- version: 7
+ version: 8
- uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 18
cache: pnpm
- run: pnpm i
- run: make docker/up db/recreate
@@ -103,7 +103,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: 16
+ node-version: 18
- uses: actions/download-artifact@v3
with:
name: coverage-unit
diff --git a/app/assets/.gitignore b/app/assets/.gitignore
deleted file mode 100644
index a5a955f965..0000000000
--- a/app/assets/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-subtitles_v1_24px.svg
diff --git a/app/assets/README.md b/app/assets/README.md
deleted file mode 100644
index fd3c4d7bd3..0000000000
--- a/app/assets/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-assets
-
-```sh
-# Download the original svg
-curl https://fonts.gstatic.com/s/i/materialicons/subtitles/v1/24px.svg > app/assets/subtitles_v1_24px.svg
-
-# Convert to different sizes
-for px in 32 192 512; do
- convert -density 1000 -resize "${px}x${px}" -background none app/assets/subtitles_v1_24px.svg "app/assets/icon-${px}.png"
-done
-```
diff --git a/app/e2e/videos.test.ts b/app/e2e/videos.test.ts
index 89f9d24dd9..6bc44fa037 100644
--- a/app/e2e/videos.test.ts
+++ b/app/e2e/videos.test.ts
@@ -224,7 +224,6 @@ test.describe("video playback rate", () => {
await page
.getByTestId("PlaybackRateSelect")
.selectOption({ label: "0.75" });
- await page.pause();
});
});
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index 09d40840ef..89d35fd319 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -1,9 +1,6 @@
import { RemixServer } from "@remix-run/react";
import type { HandleDocumentRequestFunction } from "@remix-run/server-runtime";
import { renderToString } from "react-dom/server";
-import { injectInitializeServer } from "./misc/initialize-server";
-
-injectInitializeServer();
const handler: HandleDocumentRequestFunction = (
request,
diff --git a/app/misc/initialize-server.ts b/app/misc/initialize-server.ts
index daf9ce0422..21854016bc 100644
--- a/app/misc/initialize-server.ts
+++ b/app/misc/initialize-server.ts
@@ -1,5 +1,3 @@
-import { once } from "@hiogawa/utils";
-import { installGlobals } from "@remix-run/node";
import {
finalizeDrizzleClient,
initializeDrizzleClient,
@@ -7,18 +5,12 @@ import {
import { initializeConfigServer } from "../utils/config";
import { initializeSessionStore } from "../utils/session.server";
-export const initializeServer = once(async () => {
- installGlobals();
+export async function initializeServer() {
initializeConfigServer();
initializeSessionStore();
await initializeDrizzleClient();
-});
+}
export async function finalizeServer() {
await finalizeDrizzleClient();
}
-
-// to workaround async initialization on the server (cf. @remix-run/server-runtime patch)
-export function injectInitializeServer() {
- Object.assign(globalThis, { __onRequestHandler: initializeServer });
-}
diff --git a/app/root.tsx b/app/root.tsx
index 9d1a22de82..f76c8d733b 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -40,7 +40,7 @@ export const links: LinksFunction = () => {
// prettier-ignore
return [
{ rel: "stylesheet", href: require("../build/css/index.css") },
- { rel: "icon", href: require("./assets/icon-32.png"), sizes: "32x32" },
+ { rel: "icon", href: "/favicon.ico" },
{ rel: "manifest", href: "/manifest.json" },
];
};
@@ -132,7 +132,7 @@ function Root() {
-
+ {process.env.NODE_ENV !== "production" && }
>
);
}
diff --git a/app/routes/manifest[.json].tsx b/app/routes/manifest[.json].tsx
deleted file mode 100644
index 07ca696619..0000000000
--- a/app/routes/manifest[.json].tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { prettierJson } from "../utils/loader-utils";
-import { makeLoader } from "../utils/loader-utils.server";
-
-// it could be moved to `/public` but the loader conveniently works for now.
-
-export const loader = makeLoader(({ ctx }) => {
- ctx.cacheResponse();
- return prettierJson(MANIFEST_JSON);
-});
-
-const MANIFEST_JSON = {
- short_name: "Ytsub",
- name: "Ytsub",
- icons: [
- {
- src: require("../assets/icon-192.png"),
- type: "image/png",
- sizes: "192x192",
- },
- {
- src: require("../assets/icon-512.png"),
- type: "image/png",
- sizes: "512x512",
- },
- ],
- start_url: "/",
- scope: "/",
- theme_color: "#FFFFFF",
- background_color: "#FFFFFF",
- display: "standalone",
- share_target: {
- action: "/share-target",
- method: "GET",
- enctype: "application/x-www-form-urlencoded",
- params: {
- title: "share-target-title",
- text: "share-target-text",
- url: "share-target-url",
- },
- },
-};
diff --git a/app/routes/service-worker[.js].tsx b/app/routes/service-worker[.js].tsx
deleted file mode 100644
index 56deb59144..0000000000
--- a/app/routes/service-worker[.js].tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { makeLoader } from "../utils/loader-utils.server";
-
-export const loader = makeLoader(({ ctx }) => {
- ctx.cacheResponse();
- return new Response(SERVICE_WORKER_JS, {
- headers: {
- "content-type": "application/javascript; charset=utf-8",
- },
- });
-});
-
-const SERVICE_WORKER_JS = `
-// satisfy minimal requirements for PWA
-self.addEventListener("fetch", (event) => {
- event.respondWith(fetch(event.request));
-});
-`;
diff --git a/app/routes/trpc/$trpc.tsx b/app/routes/trpc/$trpc.tsx
deleted file mode 100644
index 827f51f2bd..0000000000
--- a/app/routes/trpc/$trpc.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { ActionFunction, LoaderFunction } from "@remix-run/server-runtime";
-import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
-import { createTrpcAppContext } from "../../trpc/context";
-import { trpcApp } from "../../trpc/server";
-
-// catch-all trpc endpoint (cf. https://trpc.io/docs/server/adapters/fetch#remix)
-
-export const loader: LoaderFunction = trpcHandler;
-export const action: ActionFunction = trpcHandler;
-
-function trpcHandler(args: { request: Request }) {
- return fetchRequestHandler({
- endpoint: "/trpc",
- req: args.request,
- router: trpcApp,
- createContext: createTrpcAppContext,
- // quick error logging since otherwise remix only shows 500 access log
- onError: (e) => {
- console.error(e);
- },
- });
-}
diff --git a/app/server/entry-dev.ts b/app/server/entry-dev.ts
new file mode 100644
index 0000000000..1581aca791
--- /dev/null
+++ b/app/server/entry-dev.ts
@@ -0,0 +1,58 @@
+import fs from "node:fs";
+import { createServer } from "node:http";
+import path from "node:path";
+import process from "node:process";
+import { createMiddleware } from "@hattip/adapter-node";
+import express from "express";
+import { listenPortSearchByEnv } from "./http";
+
+async function main() {
+ //
+ // require buildPath with require.cache invalidation
+ //
+
+ const buildPath = path.resolve(process.argv[2]);
+
+ // require.cache trick as a cheap live reload
+ function requireBuild() {
+ console.log(`[entry-dev] Loading ${buildPath}`);
+ delete require.cache[buildPath];
+ return require(path.resolve(buildPath)) as typeof import("./entry-hattip");
+ }
+
+ let build = requireBuild();
+ fs.watch(path.dirname(buildPath), (eventType, filename) => {
+ if (eventType === "change" && path.basename(buildPath) === filename) {
+ build = undefined!;
+ }
+ });
+
+ //
+ // express app
+ //
+
+ const app = express();
+ const server = createServer(app);
+
+ // serve client build assets
+ app.use(
+ "/build",
+ express.static(build.assetsBuildDirectory, {
+ immutable: true,
+ maxAge: "1y",
+ })
+ );
+ app.use("/", express.static("./public"));
+
+ // main logic as hattip handler
+ app.all("*", (req, res, next) => {
+ build ??= requireBuild();
+ return createMiddleware(build.createHattipApp())(req, res, next);
+ });
+
+ // start app
+ const port = await listenPortSearchByEnv(server);
+ console.log(`[entry-dev] Server running at http://localhost:${port}`);
+}
+
+main();
diff --git a/app/server/entry-hattip.ts b/app/server/entry-hattip.ts
new file mode 100644
index 0000000000..199840ec2d
--- /dev/null
+++ b/app/server/entry-hattip.ts
@@ -0,0 +1,97 @@
+import { type RequestHandler, compose } from "@hattip/compose";
+import { once } from "@hiogawa/utils";
+import * as build from "@remix-run/dev/server-build";
+import { createRequestHandler } from "@remix-run/server-runtime";
+import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
+import type * as hono from "hono";
+import { logger } from "hono/logger";
+import { initializeServer } from "../misc/initialize-server";
+import { TRPC_ENDPOINT } from "../trpc/common";
+import { createTrpcAppContext } from "../trpc/context";
+import { trpcApp } from "../trpc/server";
+
+// based on https://github.com/hi-ogawa/vite-fullstack-example/blob/92649f99b041820ec86650c99cfcd49a72e79f71/src/server/hattip.ts#L16-L28
+
+export function createHattipApp() {
+ return compose(
+ createLogger(),
+ createBootstrapHandler(),
+ createTrpchandler(),
+ createRemixHandler()
+ );
+}
+
+// re-export for entry-dev
+// ts-prune-ignore-next
+export const assetsBuildDirectory = build.assetsBuildDirectory;
+
+//
+// remix
+//
+
+function createRemixHandler(): RequestHandler {
+ const remixHandler = createRequestHandler(build);
+ return async (ctx) => {
+ const response = await remixHandler(ctx.request);
+ return response;
+ };
+}
+
+//
+// trpc
+//
+
+function createTrpchandler(): RequestHandler {
+ return async (ctx) => {
+ if (!ctx.url.pathname.startsWith(TRPC_ENDPOINT)) {
+ return ctx.next();
+ }
+ return fetchRequestHandler({
+ endpoint: TRPC_ENDPOINT,
+ req: ctx.request,
+ router: trpcApp,
+ createContext: createTrpcAppContext,
+ onError: (e) => {
+ console.error(e);
+ },
+ });
+ };
+}
+
+//
+// bootstrap
+//
+
+function createBootstrapHandler(): RequestHandler {
+ const initializeServerOnce = once(initializeServer);
+ return async (ctx) => {
+ await initializeServerOnce();
+ return ctx.next();
+ };
+}
+
+//
+// logger
+//
+
+function createLogger(): RequestHandler {
+ // borrow hono's logger by minimal hattip-hono compatibility layer
+ // https://github.com/honojs/hono/blob/0ffd795ec6cfb67d38ab902197bb5461a4740b8f/src/middleware/logger/index.ts
+ const honoLogger = logger();
+
+ return async (ctx) => {
+ let res!: Response;
+ await honoLogger(
+ {
+ req: { method: ctx.method, raw: ctx.request },
+ get res() {
+ return res;
+ },
+ } as hono.Context,
+ async () => {
+ res = await ctx.next();
+ }
+ );
+ return res;
+ };
+}
diff --git a/app/server/entry-vercel.ts b/app/server/entry-vercel.ts
new file mode 100644
index 0000000000..61cab9b4f6
--- /dev/null
+++ b/app/server/entry-vercel.ts
@@ -0,0 +1,12 @@
+import { createMiddleware } from "@hattip/adapter-node";
+import { createHattipApp } from "./entry-hattip";
+
+// cf. https://github.com/hattipjs/hattip/blob/03a704fa120dfe2eddd6cf22eff00c90bda2acb5/packages/bundler/bundler-vercel/readme.md
+
+export default createVercelHanlder();
+
+function createVercelHanlder() {
+ return createMiddleware(createHattipApp(), {
+ trustProxy: true,
+ });
+}
diff --git a/app/server/http.ts b/app/server/http.ts
new file mode 100644
index 0000000000..a3597b768b
--- /dev/null
+++ b/app/server/http.ts
@@ -0,0 +1,52 @@
+import type { Server } from "node:http";
+import process from "node:process";
+import { newPromiseWithResolvers, range } from "@hiogawa/utils";
+
+export async function listenPortSearchByEnv(server: Server) {
+ const initialPort = Number(process.env["PORT"] ?? 3000);
+ const strictPort = Boolean(process.env["STRICT_PORT"]);
+ return listenPortSearch(server, "localhost", initialPort, strictPort);
+}
+
+async function listenPortSearch(
+ server: Server,
+ host: string,
+ initialPort: number,
+ strictPort: boolean
+) {
+ for (const port of range(initialPort, 2 ** 16)) {
+ if (port !== initialPort) {
+ console.log(`[listenPortSearch] trying next port '${port}'`);
+ }
+ try {
+ await listenPromise(server, host, port);
+ return port;
+ } catch (e) {
+ if (
+ !strictPort &&
+ e instanceof Error &&
+ (e as any).code === "EADDRINUSE"
+ ) {
+ continue;
+ }
+ throw e;
+ }
+ }
+ throw new Error(listenPortSearch.name);
+}
+
+async function listenPromise(
+ server: Server,
+ host: string,
+ port: number
+): Promise {
+ const { promise, resolve, reject } = newPromiseWithResolvers();
+ const onError = (e: unknown) => reject(e);
+ server.on("error", onError);
+ server.listen(port, host, () => resolve());
+ try {
+ await promise;
+ } finally {
+ server.off("error", onError);
+ }
+}
diff --git a/app/trpc/client-internal.client.ts b/app/trpc/client-internal.client.ts
index b073ee5988..dcf9346768 100644
--- a/app/trpc/client-internal.client.ts
+++ b/app/trpc/client-internal.client.ts
@@ -1,5 +1,6 @@
import { createTRPCProxyClient, httpLink } from "@trpc/client";
import superjson from "superjson";
+import { TRPC_ENDPOINT } from "./common";
import type { trpcApp } from "./server";
// remove raw client from server bundle since it's not meant to be used on server.
@@ -9,7 +10,7 @@ export const trpcClient = createTRPCProxyClient({
transformer: superjson,
links: [
httpLink({
- url: "/trpc",
+ url: TRPC_ENDPOINT,
}),
],
});
diff --git a/app/trpc/common.ts b/app/trpc/common.ts
new file mode 100644
index 0000000000..1b8438a6ac
--- /dev/null
+++ b/app/trpc/common.ts
@@ -0,0 +1 @@
+export const TRPC_ENDPOINT = "/trpc";
diff --git a/app/utils/config.ts b/app/utils/config.ts
index 1cb7eae02a..8ca8992d3a 100644
--- a/app/utils/config.ts
+++ b/app/utils/config.ts
@@ -1,5 +1,4 @@
import * as process from "process";
-import { once } from "@hiogawa/utils";
import { z } from "zod";
import { initializePublicConfigServer } from "./config-public";
import { uninitialized } from "./misc";
@@ -38,7 +37,7 @@ export type PublicConfig = z.infer;
export let serverConfig = uninitialized as z.infer;
-export const initializeConfigServer = once(() => {
+export function initializeConfigServer() {
serverConfig = Z_SERVER_CONFIG.parse(process.env);
initializePublicConfigServer(Z_PUBLIC_CONFIG.parse(process.env));
-});
+}
diff --git a/app/utils/session.server.ts b/app/utils/session.server.ts
index c6c57afe80..b4db912539 100644
--- a/app/utils/session.server.ts
+++ b/app/utils/session.server.ts
@@ -1,4 +1,3 @@
-import { once } from "@hiogawa/utils";
import {
Session,
SessionStorage,
@@ -8,7 +7,7 @@ import { serverConfig } from "./config";
export let sessionStore: SessionStorage;
-export const initializeSessionStore = once(() => {
+export function initializeSessionStore() {
sessionStore = createCookieSessionStorage({
cookie: {
httpOnly: true,
@@ -18,7 +17,7 @@ export const initializeSessionStore = once(() => {
secrets: [serverConfig.APP_SESSION_SECRET],
},
});
-});
+}
//
// utils
diff --git a/misc/vercel/.vc-config.json b/misc/vercel/.vc-config.json
index f2585d43df..4052d4570e 100644
--- a/misc/vercel/.vc-config.json
+++ b/misc/vercel/.vc-config.json
@@ -1,5 +1,5 @@
{
- "runtime": "nodejs16.x",
+ "runtime": "nodejs18.x",
"handler": "index.js",
"launcherType": "Nodejs",
"regions": ["hnd1"]
diff --git a/misc/vercel/build.sh b/misc/vercel/build.sh
index f1edd05510..67d983432d 100644
--- a/misc/vercel/build.sh
+++ b/misc/vercel/build.sh
@@ -10,12 +10,12 @@ set -eu -o pipefail
# project.json
# output/
# config.json
-# static/ (= (remix-output)/public)
+# static/ = (remix-outdir)/public + (root)/public
# functions/
# index.func/
# .vc-config.json
-# index-bootstrap.js (require index.js after process.setSourceMapsEnabled)
-# index.js
+# bootstrap.js
+# index.js = (remix-outdir)/server/index.js
#
# cleanup
@@ -23,25 +23,21 @@ rm -rf build/remix/production
rm -rf build/css
rm -rf .vercel/output
mkdir -p .vercel/output/functions/index.func
+mkdir -p .vercel/output/static
# css
pnpm build:css
-# remix's default "node-cjs" build with custom server entry
-NODE_ENV=production BUILD_VERCEL=1 npx remix build --sourcemap
-
-# bundle server entry
-node -r esbuild-register ./misc/vercel/bundle.ts build/remix/production/server/index.js .vercel/output/functions/index.func/index.js
+# remix build with custom server entry
+NODE_ENV=production BUILD_VERCEL=1 npx remix build
# config.json
cp misc/vercel/config.json .vercel/output/config.json
# static
-cp -r ./build/remix/production/public .vercel/output/static
+cp -a ./public/. .vercel/output/static/
+cp -a ./build/remix/production/public/. .vercel/output/static/
# serverless
-cp misc/vercel/.vc-config.json .vercel/output/functions/index.func/.vc-config.json
-cat > ".vercel/output/functions/index.func/index-bootstrap.js" < {
- if (args.path.endsWith("js")) {
- return {
- contents:
- fs.readFileSync(args.path, "utf8") +
- "\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==",
- loader: "default",
- };
- }
- });
- },
- };
-}
-
-main();
diff --git a/misc/vercel/config.json b/misc/vercel/config.json
index ab1eddb2a9..0f99076cc3 100644
--- a/misc/vercel/config.json
+++ b/misc/vercel/config.json
@@ -7,6 +7,9 @@
"cache-control": "public, immutable, max-age=31536000"
}
},
+ {
+ "handle": "filesystem"
+ },
{
"src": "^/(.*)$",
"dest": "/"
diff --git a/package.json b/package.json
index ac55d093d3..1407961dda 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,11 @@
"scripts": {
"dev": "pnpm dev-pre && run-p dev:*",
"dev-pre": "pnpm build:css",
- "dev-e2e": "export NODE_ENV=test && pnpm dev-pre && PORT=3001 pnpm dev:remix",
+ "dev-e2e": "export NODE_ENV=test && pnpm dev-pre && PORT=3001 STRICT_PORT=true pnpm dev:remix",
"dev-ui": "vite --host",
- "dev:remix": "NODE_OPTIONS='--enable-source-maps' remix dev",
- "dev-coverage:remix": "c8 -o coverage/e2e-server -r text -r html --exclude build --exclude-after-remap node_modules/.bin/remix dev",
+ "dev:remix": "run-p remix-watch remix-dev-server",
+ "remix-watch": "remix watch",
+ "remix-dev-server": "bash scripts/dev-server.sh",
"tsc": "tsc -b",
"dev:tsc": "pnpm tsc --watch --preserveWatchOutput",
"build:css": "unocss 'app/**/*.tsx' --out-file ./build/css/index.css",
@@ -42,6 +43,7 @@
"@badrap/bar-of-progress": "^0.2.2",
"@floating-ui/react": "^0.21.1",
"@formatjs/intl": "^2.7.1",
+ "@hattip/core": "^0.0.33",
"@headlessui/react": "^1.7.13",
"@hiogawa/utils": "1.4.2-pre.10",
"@hiogawa/utils-experimental": "^0.0.1",
@@ -75,6 +77,8 @@
"zod": "^3.21.4"
},
"devDependencies": {
+ "@hattip/adapter-node": "^0.0.33",
+ "@hattip/compose": "^0.0.34",
"@hiogawa/isort-ts": "1.0.2-pre.1",
"@hiogawa/unocss-preset-antd": "2.2.1-pre.2",
"@iconify-json/ri": "^1.1.7",
@@ -83,6 +87,7 @@
"@remix-run/serve": "1.15.0",
"@tsconfig/strictest": "^1.0.2",
"@types/bcryptjs": "^2.4.2",
+ "@types/express": "^4.17.17",
"@types/fs-extra": "^9.0.13",
"@types/node": "^16",
"@types/qs": "^6.9.7",
@@ -102,6 +107,7 @@
"fs-extra": "^10.1.0",
"gh-pages": "^3.2.3",
"happy-dom": "^8.9.0",
+ "hono": "^3.2.2",
"node-mailjet": "^6.0.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.4",
@@ -115,13 +121,13 @@
"zx": "^7.2.2"
},
"volta": {
- "node": "16.20.0"
+ "node": "18.16.0"
},
"pnpm": {
"patchedDependencies": {
"@remix-run/dev@1.15.0": "patches/@remix-run__dev@1.15.0.patch",
- "@remix-run/server-runtime@1.15.0": "patches/@remix-run__server-runtime@1.15.0.patch",
- "@remix-run/node@1.15.0": "patches/@remix-run__node@1.15.0.patch"
+ "@remix-run/node@1.15.0": "patches/@remix-run__node@1.15.0.patch",
+ "@remix-run/react@1.15.0": "patches/@remix-run__react@1.15.0.patch"
}
}
}
diff --git a/patches/@remix-run__react@1.15.0.patch b/patches/@remix-run__react@1.15.0.patch
new file mode 100644
index 0000000000..4c754fe8fe
--- /dev/null
+++ b/patches/@remix-run__react@1.15.0.patch
@@ -0,0 +1,13 @@
+diff --git a/dist/components.js b/dist/components.js
+index b355788f65867c8ca672d87c7cc1d26532ca1306..03c5bd9f0ac4787c20e23c800aaa7bcef884b696 100644
+--- a/dist/components.js
++++ b/dist/components.js
+@@ -1237,7 +1237,7 @@ function convertRouterFetcherToRemixFetcher(fetcherRR) {
+ // This way devs don't have to worry about doing the NODE_ENV check themselves.
+ // If running an un-bundled server outside of `remix dev` you will still need
+ // to set the REMIX_DEV_SERVER_WS_PORT manually.
+-const LiveReload = process.env.NODE_ENV !== "development" ? () => null : function LiveReload({
++const LiveReload = false ? () => null : function LiveReload({
+ port = Number(process.env.REMIX_DEV_SERVER_WS_PORT || 8002),
+ timeoutMs = 1000,
+ nonce = undefined
\ No newline at end of file
diff --git a/patches/@remix-run__server-runtime@1.15.0.patch b/patches/@remix-run__server-runtime@1.15.0.patch
deleted file mode 100644
index 542d08c416..0000000000
--- a/patches/@remix-run__server-runtime@1.15.0.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/dist/server.js b/dist/server.js
-index fd10bc563175ba69bda9b21ccc7732b20f794900..ebb6e94785bee2bca35031977d083ad516a3ae94 100644
---- a/dist/server.js
-+++ b/dist/server.js
-@@ -29,6 +29,8 @@ const createRequestHandler = (build, mode$1) => {
- let serverMode = mode.isServerMode(mode$1) ? mode$1 : mode.ServerMode.Production;
- let staticHandler = router.createStaticHandler(dataRoutes);
- return async function requestHandler(request, loadContext = {}) {
-+ // see misc/initialize-server.ts
-+ await globalThis.__onRequestHandler?.();
- let url = new URL(request.url);
-
- // special __REMIX_ASSETS_MANIFEST endpoint for checking if app server serving up-to-date routes and assets
\ No newline at end of file
diff --git a/playwright.config.ts b/playwright.config.ts
index 31fac31ec2..33a4d15065 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -7,15 +7,15 @@ export default defineConfig({
baseURL: "http://localhost:3001",
actionTimeout: 10_000,
navigationTimeout: 10_000,
- trace: process.env.E2E_CLIENT_TRACE ? "on" : "off",
+ trace: process.env.E2E_CLIENT_TRACE ? "retain-on-failure" : "off",
},
projects: [
{
name: "chromium",
use: {
browserName: "chromium",
- // https://github.com/microsoft/playwright/issues/1086#issuecomment-592227413
- viewport: null, // adopt to browser window size specified below
+ // adapt to browser window size specified below (cf. https://github.com/microsoft/playwright/issues/1086#issuecomment-592227413)
+ viewport: null,
launchOptions: {
args: ["--window-size=600,800"],
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 531d61cb28..cc7a8a5cab 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,9 +7,9 @@ patchedDependencies:
'@remix-run/node@1.15.0':
hash: z6mxtllsu73w4y6xnddlyl6j6y
path: patches/@remix-run__node@1.15.0.patch
- '@remix-run/server-runtime@1.15.0':
- hash: qalbsgt3akixld37vecooshrwu
- path: patches/@remix-run__server-runtime@1.15.0.patch
+ '@remix-run/react@1.15.0':
+ hash: ihwj5xhtucgszzff6swnc244hq
+ path: patches/@remix-run__react@1.15.0.patch
dependencies:
'@badrap/bar-of-progress':
@@ -21,6 +21,9 @@ dependencies:
'@formatjs/intl':
specifier: ^2.7.1
version: 2.7.1(typescript@4.9.5)
+ '@hattip/core':
+ specifier: ^0.0.33
+ version: 0.0.33
'@headlessui/react':
specifier: ^1.7.13
version: 1.7.13(react-dom@18.2.0)(react@18.2.0)
@@ -41,10 +44,10 @@ dependencies:
version: 1.15.0(patch_hash=z6mxtllsu73w4y6xnddlyl6j6y)
'@remix-run/react':
specifier: 1.15.0
- version: 1.15.0(react-dom@18.2.0)(react@18.2.0)
+ version: 1.15.0(patch_hash=ihwj5xhtucgszzff6swnc244hq)(react-dom@18.2.0)(react@18.2.0)
'@remix-run/server-runtime':
specifier: 1.15.0
- version: 1.15.0(patch_hash=qalbsgt3akixld37vecooshrwu)
+ version: 1.15.0
'@remix-run/v1-route-convention':
specifier: ^0.1.1
version: 0.1.1(@remix-run/dev@1.15.0)
@@ -116,6 +119,12 @@ dependencies:
version: 3.21.4
devDependencies:
+ '@hattip/adapter-node':
+ specifier: ^0.0.33
+ version: 0.0.33
+ '@hattip/compose':
+ specifier: ^0.0.34
+ version: 0.0.34
'@hiogawa/isort-ts':
specifier: 1.0.2-pre.1
version: 1.0.2-pre.1(prettier@2.8.4)(typescript@4.9.5)
@@ -140,6 +149,9 @@ devDependencies:
'@types/bcryptjs':
specifier: ^2.4.2
version: 2.4.2
+ '@types/express':
+ specifier: ^4.17.17
+ version: 4.17.17
'@types/fs-extra':
specifier: ^9.0.13
version: 9.0.13
@@ -197,6 +209,9 @@ devDependencies:
happy-dom:
specifier: ^8.9.0
version: 8.9.0
+ hono:
+ specifier: ^3.2.2
+ version: 3.2.2
node-mailjet:
specifier: ^6.0.2
version: 6.0.2
@@ -2114,6 +2129,33 @@ packages:
/@gar/promisify@1.1.3:
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
+ /@hattip/adapter-node@0.0.33:
+ resolution: {integrity: sha512-tEEYo/V0Vj7kQRuruoBHUzfJnaoKjW5EHALsU9RgyPvOs1hCJUil9UEkyVebneTBdQHUcljx4a4ZgS2pnrWOAA==}
+ dependencies:
+ '@hattip/core': 0.0.33
+ '@hattip/polyfills': 0.0.33
+ dev: true
+
+ /@hattip/compose@0.0.34:
+ resolution: {integrity: sha512-faW5gIvyDmut3/lUWslHitH2+rrtkM9IQvBOmdTjS7r9vaADVh2t/M+MgitTzsSavfQUbP+uCbTPQ+ffrUhceA==}
+ dependencies:
+ '@hattip/core': 0.0.34
+ dev: true
+
+ /@hattip/core@0.0.33:
+ resolution: {integrity: sha512-/3hhN1PYMwDDSF1zqVHEHgFHhwB2YxDPBByLZFPQG6euPTODxG9/RMpZ9YJ4cYatshSAGGwSfNXUVpflMLbJTg==}
+
+ /@hattip/core@0.0.34:
+ resolution: {integrity: sha512-L9MRB5fVgW8vd2wKDbD1pIhsM4UloRCdsXn3x9us2Xp1jeynS83T3gHGLdUvPclgGvQZapwBgUkkn9paS+bjDg==}
+ dev: true
+
+ /@hattip/polyfills@0.0.33:
+ resolution: {integrity: sha512-8rRQv/4F1xDionmk+T42lQ3tq6tQ4/+NoYaciKx0eg0C9KL1+of5BHGDkZA7XpymeoTZXsdY1hmWusX5w/8lTg==}
+ dependencies:
+ '@hattip/core': 0.0.33
+ node-fetch-native: 1.0.2
+ dev: true
+
/@headlessui/react@1.7.13(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-9n+EQKRtD9266xIHXdY5MfiXPDfYwl7zBM7KOx2Ae3Gdgxy8QML1FkCMjq6AsOf0l6N9uvI4HcFtuFlenaldKg==}
engines: {node: '>=10'}
@@ -2328,7 +2370,7 @@ packages:
'@esbuild-plugins/node-modules-polyfill': 0.1.4(esbuild@0.16.3)
'@npmcli/package-json': 2.0.0
'@remix-run/serve': 1.15.0
- '@remix-run/server-runtime': 1.15.0(patch_hash=qalbsgt3akixld37vecooshrwu)
+ '@remix-run/server-runtime': 1.15.0
'@vanilla-extract/integration': 6.2.1(@types/node@16.11.35)
arg: 5.0.2
cacache: 15.3.0
@@ -2398,7 +2440,7 @@ packages:
resolution: {integrity: sha512-CS0p8T6A2KvMoAW5zzLA/BtNNCsv34A5RJoouJvXK9/o6MriAQ/YSugg6ldS5mec49neSep+CGeL1RS6tL+3NQ==}
engines: {node: '>=14'}
dependencies:
- '@remix-run/server-runtime': 1.15.0(patch_hash=qalbsgt3akixld37vecooshrwu)
+ '@remix-run/server-runtime': 1.15.0
'@remix-run/web-fetch': 4.3.3
'@remix-run/web-file': 3.0.2
'@remix-run/web-stream': 1.0.3
@@ -2409,7 +2451,7 @@ packages:
stream-slice: 0.1.2
patched: true
- /@remix-run/react@1.15.0(react-dom@18.2.0)(react@18.2.0):
+ /@remix-run/react@1.15.0(patch_hash=ihwj5xhtucgszzff6swnc244hq)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-S0RuIeHvQTqryCZ3KVl8EsIWCqL6/ky1/kmDpN2n5Pdjew2BLC6DX7OdrY1ZQjbzOMHAROsZlyaSSVXCItunag==}
engines: {node: '>=14'}
peerDependencies:
@@ -2422,6 +2464,7 @@ packages:
react-router-dom: 6.10.0(react-dom@18.2.0)(react@18.2.0)
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
+ patched: true
/@remix-run/router@1.5.0:
resolution: {integrity: sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==}
@@ -2439,7 +2482,7 @@ packages:
transitivePeerDependencies:
- supports-color
- /@remix-run/server-runtime@1.15.0(patch_hash=qalbsgt3akixld37vecooshrwu):
+ /@remix-run/server-runtime@1.15.0:
resolution: {integrity: sha512-DL9xjHfYYrEcOq5VbhYtrjJUWo/nFQAT7Y+Np/oC55HokyU6cb2jGhl52nx96aAxKwaFCse5N90GeodFsRzX7w==}
engines: {node: '>=14'}
dependencies:
@@ -2450,7 +2493,6 @@ packages:
cookie: 0.4.2
set-cookie-parser: 2.4.8
source-map: 0.7.4
- patched: true
/@remix-run/v1-route-convention@0.1.1(@remix-run/dev@1.15.0):
resolution: {integrity: sha512-jmy/TbdwWdJnDaYVN0byyyKPUHlsJvZ+pUiXQqLouCEcjo7aQgF5gBGwRQP5bxlWJaIqYD4oNHiSVq/RkTziRA==}
@@ -2630,6 +2672,13 @@ packages:
resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==}
dev: true
+ /@types/body-parser@1.19.2:
+ resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
+ dependencies:
+ '@types/connect': 3.4.35
+ '@types/node': 16.11.35
+ dev: true
+
/@types/cacheable-request@6.0.3:
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
dependencies:
@@ -2648,6 +2697,12 @@ packages:
resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==}
dev: true
+ /@types/connect@3.4.35:
+ resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
+ dependencies:
+ '@types/node': 16.11.35
+ dev: true
+
/@types/cookie@0.4.1:
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
@@ -2669,6 +2724,24 @@ packages:
/@types/estree@1.0.0:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
+ /@types/express-serve-static-core@4.17.35:
+ resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==}
+ dependencies:
+ '@types/node': 16.11.35
+ '@types/qs': 6.9.7
+ '@types/range-parser': 1.2.4
+ '@types/send': 0.17.1
+ dev: true
+
+ /@types/express@4.17.17:
+ resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==}
+ dependencies:
+ '@types/body-parser': 1.19.2
+ '@types/express-serve-static-core': 4.17.35
+ '@types/qs': 6.9.7
+ '@types/serve-static': 1.15.1
+ dev: true
+
/@types/fs-extra@11.0.1:
resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==}
dependencies:
@@ -2719,6 +2792,14 @@ packages:
/@types/mdurl@1.0.2:
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
+ /@types/mime@1.3.2:
+ resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==}
+ dev: true
+
+ /@types/mime@3.0.1:
+ resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
+ dev: true
+
/@types/minimatch@5.1.2:
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
@@ -2754,6 +2835,10 @@ packages:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
dev: true
+ /@types/range-parser@1.2.4:
+ resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
+ dev: true
+
/@types/react-dom@18.0.11:
resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==}
dependencies:
@@ -2775,6 +2860,20 @@ packages:
/@types/scheduler@0.16.2:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
+ /@types/send@0.17.1:
+ resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
+ dependencies:
+ '@types/mime': 1.3.2
+ '@types/node': 16.11.35
+ dev: true
+
+ /@types/serve-static@1.15.1:
+ resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==}
+ dependencies:
+ '@types/mime': 3.0.1
+ '@types/node': 16.11.35
+ dev: true
+
/@types/showdown@2.0.1:
resolution: {integrity: sha512-xdnAw2nFqomkaL0QdtEk0t7yz26UkaVPl4v1pYJvtE1T0fmfQEH3JaxErEhGByEAl3zUZrkNBlneuJp0WJGqEA==}
dev: true
@@ -4902,7 +5001,6 @@ packages:
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- dev: true
/graceful-fs@4.2.9:
resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==}
@@ -5013,6 +5111,11 @@ packages:
'@babel/runtime': 7.17.2
dev: false
+ /hono@3.2.2:
+ resolution: {integrity: sha512-yVkYyefATYGz6j7iDcugtEvg4AgpccE1tDYnmvTmXDR4NQrUJ3/SdHU9V7UuWQjGtfrBujeBUAq70oc82ADsvA==}
+ engines: {node: '>=16.0.0'}
+ dev: true
+
/hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true
@@ -5496,14 +5599,14 @@ packages:
/jsonfile@4.0.0:
resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=}
optionalDependencies:
- graceful-fs: 4.2.9
+ graceful-fs: 4.2.11
/jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
universalify: 2.0.0
optionalDependencies:
- graceful-fs: 4.2.9
+ graceful-fs: 4.2.11
/keyv@4.5.2:
resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==}
diff --git a/app/assets/icon-32.png b/public/favicon.ico
similarity index 100%
rename from app/assets/icon-32.png
rename to public/favicon.ico
diff --git a/app/assets/icon-192.png b/public/icon-192.png
similarity index 100%
rename from app/assets/icon-192.png
rename to public/icon-192.png
diff --git a/app/assets/icon-512.png b/public/icon-512.png
similarity index 100%
rename from app/assets/icon-512.png
rename to public/icon-512.png
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000000..17099b7abe
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,31 @@
+{
+ "short_name": "Ytsub",
+ "name": "Ytsub",
+ "icons": [
+ {
+ "src": "/icon-192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "/icon-512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": "/",
+ "scope": "/",
+ "theme_color": "#FFFFFF",
+ "background_color": "#FFFFFF",
+ "display": "standalone",
+ "share_target": {
+ "action": "/share-target",
+ "method": "GET",
+ "enctype": "application/x-www-form-urlencoded",
+ "params": {
+ "title": "share-target-title",
+ "text": "share-target-text",
+ "url": "share-target-url"
+ }
+ }
+}
diff --git a/public/service-worker.js b/public/service-worker.js
new file mode 100644
index 0000000000..1ca04e1f6f
--- /dev/null
+++ b/public/service-worker.js
@@ -0,0 +1,4 @@
+// satisfy minimal requirements for PWA
+self.addEventListener("fetch", (event) => {
+ event.respondWith(fetch(event.request));
+});
diff --git a/remix.config.js b/remix.config.js
index 79a617c1ba..6b6e195475 100644
--- a/remix.config.js
+++ b/remix.config.js
@@ -5,8 +5,10 @@ const env = process.env.NODE_ENV ?? "development";
module.exports = {
serverBuildPath: `build/remix/${env}/server/index.js`,
assetsBuildDirectory: `build/remix/${env}/public/build`,
- publicPath: process.env.BUILD_VERCEL ? undefined : `/build/remix/${env}/public/build`,
- server: process.env.BUILD_VERCEL ? "./app/misc/vercel.ts" : undefined,
+ server: process.env.BUILD_VERCEL ? "./app/server/entry-vercel.ts" : "./app/server/entry-hattip.ts",
+ serverDependenciesToBundle: process.env.BUILD_VERCEL ? "all" : [
+ /@hattip/, /@js-temporal/
+ ],
future: {
v2_meta: true,
v2_errorBoundary: true,
diff --git a/scripts/dev-server.sh b/scripts/dev-server.sh
new file mode 100644
index 0000000000..4fba889f56
--- /dev/null
+++ b/scripts/dev-server.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+set -eu -o pipefail
+
+server_entry="./build/remix/${NODE_ENV:-development}/server/index.js"
+
+bash scripts/wait-for.sh test -f "$server_entry"
+
+entry_dev_cmd=(node -r esbuild-register ./app/server/entry-dev.ts "$server_entry")
+
+if [ -n "${E2E_COVERAGE_SERVER:-}" ]; then
+ exec npx c8 -o coverage/e2e-server -r text -r html --exclude build --exclude-after-remap "${entry_dev_cmd[@]}"
+else
+ exec "${entry_dev_cmd[@]}"
+fi
diff --git a/scripts/test-e2e-coverage.sh b/scripts/test-e2e-coverage.sh
index 4258ddb0e4..75345e6fbd 100644
--- a/scripts/test-e2e-coverage.sh
+++ b/scripts/test-e2e-coverage.sh
@@ -4,6 +4,7 @@ set -eux -o pipefail
export NODE_ENV=test
export PORT=3001
export E2E_NO_SERVER=1
+export E2E_COVERAGE_SERVER=1
export E2E_COVERAGE_CLIENT=1
log_file=logs/remix-coverage.log
@@ -11,7 +12,7 @@ trap 'cat "${log_file}"' EXIT
# run remix server with c8
pnpm dev-pre
-pnpm dev-coverage:remix > "$log_file" 2>&1 &
+pnpm dev:remix > "$log_file" 2>&1 &
coverage_pid="$!"
# wait server
@@ -24,7 +25,7 @@ playwright test "${@}"
npx c8 report -o coverage/e2e-client -r text -r html --exclude build --exclude-after-remap --temp-directory coverage/e2e-client/tmp
# stop remix server
-curl "http://localhost:$PORT/dev/stop"
+curl "http://localhost:$PORT/dev/stop" || true
# wait for c8 to create e2e-server coverage
wait "$coverage_pid" || true
diff --git a/scripts/wait-for.sh b/scripts/wait-for.sh
new file mode 100644
index 0000000000..caeffa3e48
--- /dev/null
+++ b/scripts/wait-for.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+set -eu -o pipefail
+
+# usage
+# bash scripts/wait-for.sh test -f build/test/server/index.js
+
+for ((i=0; ;i++)); do
+ echo "[wait-for:$i] ${*}"
+ sleep "$i"
+ if "${@}"; then
+ echo "[wait-for:$i:success] ${*}"
+ break;
+ fi
+done