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